From 21187b7c1bc7eed2c84e3a5925cfdc32393c1b58 Mon Sep 17 00:00:00 2001 From: Luca Olivetti Date: Fri, 4 Dec 2015 16:10:43 +0100 Subject: [PATCH 001/445] mageia bootstrap --- bootstrap/_mageia_common.sh | 24 ++++++++++++++++++++++++ letsencrypt-auto | 3 +++ 2 files changed, 27 insertions(+) create mode 100755 bootstrap/_mageia_common.sh diff --git a/bootstrap/_mageia_common.sh b/bootstrap/_mageia_common.sh new file mode 100755 index 000000000..9a4606c9d --- /dev/null +++ b/bootstrap/_mageia_common.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# Tested on mageia 5 x86_64 +if ! urpmi --force \ + python \ + libpython-devel \ + python-virtualenv +then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 +fi + +if ! urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts +then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 +fi diff --git a/letsencrypt-auto b/letsencrypt-auto index 44c71883c..13a966a87 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -122,6 +122,9 @@ then if [ -f /etc/debian_version ] ; then echo "Bootstrapping dependencies for Debian-based OSes..." $SUDO $BOOTSTRAP/_deb_common.sh + elif [ -f /etc/mageia-release ] ; then + echo "Bootstrapping dependencies for mageia..." + $SUDO $BOOTSTRAP/_mageia_common.sh elif [ -f /etc/redhat-release ] ; then echo "Bootstrapping dependencies for RedHat-based OSes..." $SUDO $BOOTSTRAP/_rpm_common.sh From 55ca1b484fc18626e89b111ff5e960a4081694c1 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Fri, 1 Jan 2016 20:55:52 -0500 Subject: [PATCH 002/445] Initial verison of DNS-01 implementation --- acme/acme/challenges.py | 79 ++++++++++++++++++++++++++++++ acme/acme/challenges_test.py | 93 ++++++++++++++++++++++++++++++++++++ acme/setup.py | 1 + 3 files changed, 173 insertions(+) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 1e456d325..f48c20443 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -1,5 +1,8 @@ """ACME Identifier Validation Challenges.""" import abc +import base64 +import dns.resolver +import dns.exception import functools import hashlib import logging @@ -215,6 +218,82 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): self.validation(account_key, *args, **kwargs)) +@ChallengeResponse.register +class DNS01Response(KeyAuthorizationChallengeResponse): + """ACME "dns-01" challenge response.""" + typ = "dns-01" + + def simple_verify(self, chall, domain, account_public_key): + """Simple verify. + + :param challenges.DNS01 chall: Corresponding challenge. + :param unicode domain: Domain name being verified. + :param account_public_key: Public key for the key pair + being authorized. If ``None`` key verification is not + performed! + :param JWK account_public_key: + + :returns: ``True`` iff validation is successful, ``False`` + otherwise. + :rtype: bool + + """ + if not self.verify(chall, account_public_key): + logger.debug("Verification of key authorization in response failed") + return False + + validation_name = chall.validation_domain_name(domain) + validation = chall.validation(account_public_key) + logger.debug("Verifying %s at %s...", chall.typ, validation_name) + txt_records = [] + try: + dns_response = dns.resolver.query(validation_name, 'TXT') + for rdata in dns_response: + for txt_record in rdata.strings: + txt_records.append(txt_record) + except dns.exception.DNSException as error: + logger.error("Unable to resolve %s: %s", validation_name, error) + return False + + for txt_record in txt_records: + if txt_record == validation: + return True + + logger.debug("Key authorization from response (%r) doesn't match any " + "DNS response in %r", self.key_authorization, txt_records) + return False + +@Challenge.register # pylint: disable=too-many-ancestors +class DNS01(KeyAuthorizationChallenge): + """ACME "dns-01" challenge.""" + + response_cls = DNS01Response + typ = response_cls.typ + + LABEL = "_acme-challenge" + """Label clients prepend to the domain name being validated.""" + + def validation(self, account_key, **unused_kwargs): + """Generate validation. + + :param JWK account_key: + :rtype: unicode + + """ + key_authorization = self.key_authorization(account_key) + # FIXME Once boulder response according to the spec this needs to be fixed + # return base64.b64encode(hashlib.sha256(key_authorization).digest()) + return hashlib.sha256(key_authorization).hexdigest() + + def validation_domain_name(self, name): + """Domain name for TXT validation record. + + :param unicode name: Domain name being validated. + + """ + return "{0}.{1}".format(self.LABEL, name) + + @ChallengeResponse.register class HTTP01Response(KeyAuthorizationChallengeResponse): """ACME http-01 challenge response.""" diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index a4e78ebe9..a587d3f61 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -1,6 +1,7 @@ """Tests for acme.challenges.""" import unittest +import dns.rrset import mock import OpenSSL import requests @@ -76,6 +77,98 @@ class KeyAuthorizationChallengeResponseTest(unittest.TestCase): key_authorization='.foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') self.assertFalse(response.verify(self.chall, KEY.public_key())) +class DNS01ResponseTest(unittest.TestCase): + # pylint: disable=too-many-instance-attributes + + def setUp(self): + from acme.challenges import DNS01Response + self.msg = DNS01Response(key_authorization=u'foo') + self.jmsg = { + 'resource': 'challenge', + 'type': 'dns-01', + 'keyAuthorization': u'foo', + } + + from acme.challenges import DNS01 + self.chall = DNS01(token=(b'x' * 16)) + self.response = self.chall.response(KEY) + + # This takes advantage of the fact that an answer object mostly behaves like + # an RRset + def create_txt_response(self, name, txt_record): + return dns.rrset.from_text(name, 60, "IN", "TXT", txt_record) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import DNS01Response + self.assertEqual( + self.msg, DNS01Response.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import DNS01Response + hash(DNS01Response.from_json(self.jmsg)) + + def test_simple_verify_bad_key_authorization(self): + key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) + self.response.simple_verify(self.chall, "local", key2.public_key()) + + @mock.patch("acme.challenges.dns.resolver.query") + def test_simple_verify_good_validation(self, mock_dns): + mock_dns.return_value = self.create_txt_response( + self.chall.validation_domain_name("local"), + self.chall.validation(KEY.public_key())) + self.assertTrue(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + mock_dns.assert_called_once_with( + self.chall.validation_domain_name("local"), "TXT") + + @mock.patch("acme.challenges.dns.resolver.query") + def test_simple_verify_bad_validation(self, mock_dns): + mock_dns.return_value = self.create_txt_response( + self.chall.validation_domain_name("local"), "!") + self.assertFalse(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + + @mock.patch("acme.challenges.dns.resolver.query") + def test_simple_verify_connection_error(self, mock_dns): + mock_dns.side_effect = dns.exception.DNSException + self.assertFalse(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + +class DNS01Test(unittest.TestCase): + + def setUp(self): + from acme.challenges import DNS01 + self.msg = DNS01( + token=jose.decode_b64jose( + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) + self.jmsg = { + 'type': 'dns-01', + 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', + } + + def test_validation_domain_name(self): + self.assertEqual('_acme-challenge.www.example.com', + self.msg.validation_domain_name('www.example.com')) + + def test_validation(self): + self.assertEqual( + "ac06bb8888382b6cbaddfbd48427f2f1d3f55e5ef0121990ab4a02853704dd99", + self.msg.validation(KEY)) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import DNS01 + self.assertEqual(self.msg, DNS01.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import DNS01 + hash(DNS01.from_json(self.jmsg)) + class HTTP01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes diff --git a/acme/setup.py b/acme/setup.py index ba2c88394..dd2bce5d9 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -12,6 +12,7 @@ install_requires = [ 'cryptography>=0.8', # Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15) 'PyOpenSSL>=0.15', + 'dnspython', 'pyrfc3339', 'pytz', 'requests', From ffc2b1ee7864a848c0f2a22ca873ac970bb911bf Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 2 Jan 2016 01:42:47 -0500 Subject: [PATCH 003/445] - Lint fixes - Add test for multiple TXT records returned - Add extra parameter in DNS01.validation to select hexdigit vs. bas64 encoded validation --- acme/acme/challenges.py | 23 +++++++++++------------ acme/acme/challenges_test.py | 36 +++++++++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index f48c20443..6c13ec906 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -242,17 +242,15 @@ class DNS01Response(KeyAuthorizationChallengeResponse): logger.debug("Verification of key authorization in response failed") return False - validation_name = chall.validation_domain_name(domain) + validation_domain_name = chall.validation_domain_name(domain) validation = chall.validation(account_public_key) - logger.debug("Verifying %s at %s...", chall.typ, validation_name) - txt_records = [] + logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) try: - dns_response = dns.resolver.query(validation_name, 'TXT') - for rdata in dns_response: - for txt_record in rdata.strings: - txt_records.append(txt_record) + dns_response = dns.resolver.query(validation_domain_name, 'TXT') + txt_records = sum([rdata.strings for rdata in dns_response], []) except dns.exception.DNSException as error: - logger.error("Unable to resolve %s: %s", validation_name, error) + logger.error("Unable to resolve %s: %s", validation_domain_name, + error) return False for txt_record in txt_records: @@ -273,7 +271,8 @@ class DNS01(KeyAuthorizationChallenge): LABEL = "_acme-challenge" """Label clients prepend to the domain name being validated.""" - def validation(self, account_key, **unused_kwargs): + # FIXME: Remove extra parameter once #2052 is integrated + def validation(self, account_key, dns01_hexdigit_response=True, **unused_kwargs): """Generate validation. :param JWK account_key: @@ -281,9 +280,9 @@ class DNS01(KeyAuthorizationChallenge): """ key_authorization = self.key_authorization(account_key) - # FIXME Once boulder response according to the spec this needs to be fixed - # return base64.b64encode(hashlib.sha256(key_authorization).digest()) - return hashlib.sha256(key_authorization).hexdigest() + if dns01_hexdigit_response: + return hashlib.sha256(key_authorization).hexdigest() + return base64.urlsafe_b64encode(hashlib.sha256(key_authorization).digest()) def validation_domain_name(self, name): """Domain name for TXT validation record. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index a587d3f61..c2a629bb3 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -93,10 +93,15 @@ class DNS01ResponseTest(unittest.TestCase): self.chall = DNS01(token=(b'x' * 16)) self.response = self.chall.response(KEY) - # This takes advantage of the fact that an answer object mostly behaves like - # an RRset - def create_txt_response(self, name, txt_record): - return dns.rrset.from_text(name, 60, "IN", "TXT", txt_record) + def create_txt_response(self, name, txt_records): + """ + Returns an RRSet containing the 'txt_records' as the result of a DNS + query for 'name'. + + This takes advantage of the fact that an Answer object mostly behaves + like an RRset. + """ + return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) @@ -118,7 +123,17 @@ class DNS01ResponseTest(unittest.TestCase): def test_simple_verify_good_validation(self, mock_dns): mock_dns.return_value = self.create_txt_response( self.chall.validation_domain_name("local"), - self.chall.validation(KEY.public_key())) + [self.chall.validation(KEY.public_key())]) + self.assertTrue(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + mock_dns.assert_called_once_with( + self.chall.validation_domain_name("local"), "TXT") + + @mock.patch("acme.challenges.dns.resolver.query") + def test_simple_verify_good_validation_multiple_txts(self, mock_dns): + mock_dns.return_value = self.create_txt_response( + self.chall.validation_domain_name("local"), + ["!", self.chall.validation(KEY.public_key())]) self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_dns.assert_called_once_with( @@ -127,7 +142,7 @@ class DNS01ResponseTest(unittest.TestCase): @mock.patch("acme.challenges.dns.resolver.query") def test_simple_verify_bad_validation(self, mock_dns): mock_dns.return_value = self.create_txt_response( - self.chall.validation_domain_name("local"), "!") + self.chall.validation_domain_name("local"), ["!"]) self.assertFalse(self.response.simple_verify( self.chall, "local", KEY.public_key())) @@ -153,10 +168,17 @@ class DNS01Test(unittest.TestCase): self.assertEqual('_acme-challenge.www.example.com', self.msg.validation_domain_name('www.example.com')) + # FIXME: Remove extra parameter once #2052 is integrated def test_validation(self): + self.assertEqual( + "rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk=", + self.msg.validation(KEY, dns01_hexdigit_response=False)) + + # FIXME: Remove this once #2052 is integrated + def test_validation_for_server_with_hexdigit_response(self): self.assertEqual( "ac06bb8888382b6cbaddfbd48427f2f1d3f55e5ef0121990ab4a02853704dd99", - self.msg.validation(KEY)) + self.msg.validation(KEY, dns01_hexdigit_response=True)) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) From 7e2a1532ef5a3c48e3d1cf9c382a13da0ed62097 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 2 Jan 2016 12:53:47 -0500 Subject: [PATCH 004/445] Move dns record retrieval into a separate method. --- acme/acme/challenges.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 6c13ec906..cc938dd31 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -223,6 +223,22 @@ class DNS01Response(KeyAuthorizationChallengeResponse): """ACME "dns-01" challenge response.""" typ = "dns-01" + def txt_records_for_name(self, name): + """Resolve the name and return the TXT records. + + :param unicode name: Domain name being verified. + + :returns: A list of txt records, or None if the name could not be resolved + :rtype: list of unicode + + """ + try: + dns_response = dns.resolver.query(name, 'TXT') + except dns.exception.DNSException as error: + logger.error("Unable to resolve %s: %s", name, error) + return None + return sum([rdata.strings for rdata in dns_response], []) + def simple_verify(self, chall, domain, account_public_key): """Simple verify. @@ -245,15 +261,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation_domain_name = chall.validation_domain_name(domain) validation = chall.validation(account_public_key) logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) - try: - dns_response = dns.resolver.query(validation_domain_name, 'TXT') - txt_records = sum([rdata.strings for rdata in dns_response], []) - except dns.exception.DNSException as error: - logger.error("Unable to resolve %s: %s", validation_domain_name, - error) - return False - - for txt_record in txt_records: + for txt_record in self.txt_records_for_domain(validation_domain_name): if txt_record == validation: return True From 64f3f53467b33d97359d0ba6f9795d5757d3f4a7 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 2 Jan 2016 13:51:37 -0500 Subject: [PATCH 005/445] Fix --- acme/acme/challenges.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index cc938dd31..5c3c8e8aa 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -261,7 +261,11 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation_domain_name = chall.validation_domain_name(domain) validation = chall.validation(account_public_key) logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) - for txt_record in self.txt_records_for_domain(validation_domain_name): + txt_records = self.txt_records_for_name(validation_domain_name) + if txt_records == None: + return False + + for txt_record in txt_records: if txt_record == validation: return True From 97fb1a03f99cd219cffe7e77ab6c616c03b9afec Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 3 Jan 2016 13:19:32 -0500 Subject: [PATCH 006/445] Documentation fixes. --- acme/acme/challenges.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 5c3c8e8aa..6ca36d482 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -249,8 +249,8 @@ class DNS01Response(KeyAuthorizationChallengeResponse): performed! :param JWK account_public_key: - :returns: ``True`` iff validation is successful, ``False`` - otherwise. + :returns: ``True`` iff validation with the TXT records resolved from a + DNS server is successful. :rtype: bool """ @@ -332,8 +332,8 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): :param JWK account_public_key: :param int port: Port used in the validation. - :returns: ``True`` iff validation is successful, ``False`` - otherwise. + :returns: ``True`` iff validation of the files currently server by the + HTTP server is successful. :rtype: bool """ @@ -504,7 +504,7 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse): :returns: ``True`` iff client's control of the domain has been - verified, ``False`` otherwise. + verified. :rtype: bool """ From 74a9703269a320a0180e8360218e6f8de599d0ba Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Mon, 4 Jan 2016 16:16:42 -0500 Subject: [PATCH 007/445] Style fix --- acme/acme/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 6ca36d482..b783eee9c 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -262,7 +262,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation = chall.validation(account_public_key) logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) txt_records = self.txt_records_for_name(validation_domain_name) - if txt_records == None: + if txt_records is None: return False for txt_record in txt_records: From 7747dc8488f2ea67b21ad53ffe9e31cd8e7d1232 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Mon, 4 Jan 2016 19:46:28 -0500 Subject: [PATCH 008/445] Remove non-compliant hexdigit encoding for dns-01 challenges (#2052 is now merged). --- acme/acme/challenges.py | 8 ++------ acme/acme/challenges_test.py | 11 ++--------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 6ca36d482..77ca2e214 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -1,6 +1,5 @@ """ACME Identifier Validation Challenges.""" import abc -import base64 import dns.resolver import dns.exception import functools @@ -283,8 +282,7 @@ class DNS01(KeyAuthorizationChallenge): LABEL = "_acme-challenge" """Label clients prepend to the domain name being validated.""" - # FIXME: Remove extra parameter once #2052 is integrated - def validation(self, account_key, dns01_hexdigit_response=True, **unused_kwargs): + def validation(self, account_key, **unused_kwargs): """Generate validation. :param JWK account_key: @@ -292,9 +290,7 @@ class DNS01(KeyAuthorizationChallenge): """ key_authorization = self.key_authorization(account_key) - if dns01_hexdigit_response: - return hashlib.sha256(key_authorization).hexdigest() - return base64.urlsafe_b64encode(hashlib.sha256(key_authorization).digest()) + return jose.b64encode(hashlib.sha256(key_authorization).digest()) def validation_domain_name(self, name): """Domain name for TXT validation record. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index c2a629bb3..b6c771d1b 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -168,17 +168,10 @@ class DNS01Test(unittest.TestCase): self.assertEqual('_acme-challenge.www.example.com', self.msg.validation_domain_name('www.example.com')) - # FIXME: Remove extra parameter once #2052 is integrated def test_validation(self): self.assertEqual( - "rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk=", - self.msg.validation(KEY, dns01_hexdigit_response=False)) - - # FIXME: Remove this once #2052 is integrated - def test_validation_for_server_with_hexdigit_response(self): - self.assertEqual( - "ac06bb8888382b6cbaddfbd48427f2f1d3f55e5ef0121990ab4a02853704dd99", - self.msg.validation(KEY, dns01_hexdigit_response=True)) + "rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk", + self.msg.validation(KEY)) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) From b5bb90628c080d7719546438d73b01534c7db25a Mon Sep 17 00:00:00 2001 From: wteiken Date: Tue, 5 Jan 2016 20:33:30 -0500 Subject: [PATCH 009/445] Style changes. --- acme/acme/challenges.py | 30 ++++++++++++------------------ acme/acme/challenges_test.py | 4 ++-- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 77ca2e214..cb8873984 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -1,13 +1,13 @@ """ACME Identifier Validation Challenges.""" import abc -import dns.resolver -import dns.exception import functools import hashlib import logging import socket from cryptography.hazmat.primitives import hashes +import dns.resolver +import dns.exception import OpenSSL import requests @@ -217,17 +217,16 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): self.validation(account_key, *args, **kwargs)) -@ChallengeResponse.register class DNS01Response(KeyAuthorizationChallengeResponse): """ACME "dns-01" challenge response.""" typ = "dns-01" - def txt_records_for_name(self, name): + def txt_records_for_name(name): """Resolve the name and return the TXT records. :param unicode name: Domain name being verified. - :returns: A list of txt records, or None if the name could not be resolved + :returns: A list of txt records, if empty the name could not be resolved :rtype: list of unicode """ @@ -235,17 +234,17 @@ class DNS01Response(KeyAuthorizationChallengeResponse): dns_response = dns.resolver.query(name, 'TXT') except dns.exception.DNSException as error: logger.error("Unable to resolve %s: %s", name, error) - return None - return sum([rdata.strings for rdata in dns_response], []) + return [] + return [txt_rec in dns_response for txt_rec in rdata.strings] +@ChallengeResponse.register def simple_verify(self, chall, domain, account_public_key): """Simple verify. :param challenges.DNS01 chall: Corresponding challenge. :param unicode domain: Domain name being verified. :param account_public_key: Public key for the key pair - being authorized. If ``None`` key verification is not - performed! + being authorized. :param JWK account_public_key: :returns: ``True`` iff validation with the TXT records resolved from a @@ -260,14 +259,9 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation_domain_name = chall.validation_domain_name(domain) validation = chall.validation(account_public_key) logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) - txt_records = self.txt_records_for_name(validation_domain_name) - if txt_records == None: - return False - - for txt_record in txt_records: - if txt_record == validation: - return True + if validation in txt_records_for_name(validation_domain_name): + return True logger.debug("Key authorization from response (%r) doesn't match any " "DNS response in %r", self.key_authorization, txt_records) return False @@ -289,8 +283,8 @@ class DNS01(KeyAuthorizationChallenge): :rtype: unicode """ - key_authorization = self.key_authorization(account_key) - return jose.b64encode(hashlib.sha256(key_authorization).digest()) + return jose.b64encode(hashlib.sha256( + self.key_authorization(account_key)).digest()) def validation_domain_name(self, name): """Domain name for TXT validation record. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index b6c771d1b..b744de024 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -77,6 +77,7 @@ class KeyAuthorizationChallengeResponseTest(unittest.TestCase): key_authorization='.foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') self.assertFalse(response.verify(self.chall, KEY.public_key())) + class DNS01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes @@ -108,8 +109,7 @@ class DNS01ResponseTest(unittest.TestCase): def test_from_json(self): from acme.challenges import DNS01Response - self.assertEqual( - self.msg, DNS01Response.from_json(self.jmsg)) + self.assertEqual(self.msg, DNS01Response.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import DNS01Response From 4403a78e52e684d5b94b8982f5b86609fc1fc20f Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Tue, 5 Jan 2016 22:25:24 -0500 Subject: [PATCH 010/445] Move txt_records_for_name out of class. --- acme/acme/challenges.py | 49 +++++++++++++++++++----------------- acme/acme/challenges_test.py | 1 + 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index cb8873984..e235a087e 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -217,27 +217,11 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): self.validation(account_key, *args, **kwargs)) +@ChallengeResponse.register class DNS01Response(KeyAuthorizationChallengeResponse): """ACME "dns-01" challenge response.""" typ = "dns-01" - def txt_records_for_name(name): - """Resolve the name and return the TXT records. - - :param unicode name: Domain name being verified. - - :returns: A list of txt records, if empty the name could not be resolved - :rtype: list of unicode - - """ - try: - dns_response = dns.resolver.query(name, 'TXT') - except dns.exception.DNSException as error: - logger.error("Unable to resolve %s: %s", name, error) - return [] - return [txt_rec in dns_response for txt_rec in rdata.strings] - -@ChallengeResponse.register def simple_verify(self, chall, domain, account_public_key): """Simple verify. @@ -260,16 +244,18 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation = chall.validation(account_public_key) logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) - if validation in txt_records_for_name(validation_domain_name): - return True - logger.debug("Key authorization from response (%r) doesn't match any " - "DNS response in %r", self.key_authorization, txt_records) - return False + txt_records = txt_records_for_name(validation_domain_name) + exists = validation in txt_records + if not exists: + logger.debug("Key authorization from response (%r) doesn't match " + "any DNS response in %r", self.key_authorization, + txt_records) + return exists + @Challenge.register # pylint: disable=too-many-ancestors class DNS01(KeyAuthorizationChallenge): """ACME "dns-01" challenge.""" - response_cls = DNS01Response typ = response_cls.typ @@ -716,3 +702,20 @@ class DNSResponse(ChallengeResponse): """ return chall.check_validation(self.validation, account_public_key) + + +def txt_records_for_name(name): + """Resolve the name and return the TXT records. + + :param unicode name: Domain name being verified. + + :returns: A list of txt records, if empty the name could not be resolved + :rtype: list of unicode + + """ + try: + dns_response = dns.resolver.query(name, 'TXT') + except dns.exception.DNSException as error: + logger.error("Unable to resolve %s: %s", name, error) + return [] + return [txt_rec for rdata in dns_response for txt_rec in rdata.strings] diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index b744de024..79a928456 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -152,6 +152,7 @@ class DNS01ResponseTest(unittest.TestCase): self.assertFalse(self.response.simple_verify( self.chall, "local", KEY.public_key())) + class DNS01Test(unittest.TestCase): def setUp(self): From fd2709a6fa2c2cc764aa2b1a50178ad62e71a1c5 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Tue, 5 Jan 2016 23:58:23 -0500 Subject: [PATCH 011/445] Move dnspython dependency to tests only and only import the dns.resolver when actually resolving the client. That way user code that does not call 'simple_verify' for DNS01 challenges does not depend on dnspython. --- acme/acme/challenges.py | 10 ++++++---- acme/setup.py | 3 +-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index e235a087e..aa7b20689 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -6,8 +6,6 @@ import logging import socket from cryptography.hazmat.primitives import hashes -import dns.resolver -import dns.exception import OpenSSL import requests @@ -714,8 +712,12 @@ def txt_records_for_name(name): """ try: + import dns.resolver dns_response = dns.resolver.query(name, 'TXT') - except dns.exception.DNSException as error: - logger.error("Unable to resolve %s: %s", name, error) + except ImportError as error: + raise ImportError("Local validation for 'dns-01' challenges requires " + "'dnspython'"); + except Exception as error: + logger.error("Unable to resolve %s: %s", name, str(error)) return [] return [txt_rec for rdata in dns_response for txt_rec in rdata.strings] diff --git a/acme/setup.py b/acme/setup.py index dd2bce5d9..76a2c1b72 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -12,7 +12,6 @@ install_requires = [ 'cryptography>=0.8', # Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15) 'PyOpenSSL>=0.15', - 'dnspython', 'pyrfc3339', 'pytz', 'requests', @@ -76,7 +75,7 @@ setup( install_requires=install_requires, extras_require={ 'docs': docs_extras, - 'testing': testing_extras, + 'testing': testing_extras + 'dnspython', }, entry_points={ 'console_scripts': [ From 57c265c7f36087bb3de0f96a25cfcd8ae62538ab Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 00:27:07 -0500 Subject: [PATCH 012/445] Setup.py and style fixes --- acme/acme/challenges.py | 5 +++-- acme/setup.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index aa7b20689..f4eb115c4 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -713,11 +713,12 @@ def txt_records_for_name(name): """ try: import dns.resolver + import dns.exception dns_response = dns.resolver.query(name, 'TXT') except ImportError as error: raise ImportError("Local validation for 'dns-01' challenges requires " - "'dnspython'"); - except Exception as error: + "'dnspython'") + except dns.exception.DNSException as error: logger.error("Unable to resolve %s: %s", name, str(error)) return [] return [txt_rec for rdata in dns_response for txt_rec in rdata.strings] diff --git a/acme/setup.py b/acme/setup.py index 76a2c1b72..e00476117 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -75,7 +75,7 @@ setup( install_requires=install_requires, extras_require={ 'docs': docs_extras, - 'testing': testing_extras + 'dnspython', + 'testing': testing_extras + ['dnspython'], }, entry_points={ 'console_scripts': [ From dc743fb57cc9c3c7fb805ece50681f1388440f9c Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 01:11:24 -0500 Subject: [PATCH 013/445] Move DNS resolver to separate module to decouple dependencies and testing. --- acme/acme/challenges.py | 30 ++++++---------------- acme/acme/challenges_test.py | 46 +++++++++------------------------- acme/acme/dns_resolver.py | 28 +++++++++++++++++++++ acme/acme/dns_resolver_test.py | 37 +++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 57 deletions(-) create mode 100644 acme/acme/dns_resolver.py create mode 100644 acme/acme/dns_resolver_test.py diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index f4eb115c4..221eb406c 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -15,7 +15,6 @@ from acme import fields from acme import jose from acme import other - logger = logging.getLogger(__name__) @@ -242,7 +241,13 @@ class DNS01Response(KeyAuthorizationChallengeResponse): validation = chall.validation(account_public_key) logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name) - txt_records = txt_records_for_name(validation_domain_name) + try: + from acme import dns_resolver + txt_records = dns_resolver.txt_records_for_name( + validation_domain_name) + except ImportError as error: + raise ImportError("Local validation for 'dns-01' challenges " + "requires 'dnspython'") exists = validation in txt_records if not exists: logger.debug("Key authorization from response (%r) doesn't match " @@ -701,24 +706,3 @@ class DNSResponse(ChallengeResponse): """ return chall.check_validation(self.validation, account_public_key) - -def txt_records_for_name(name): - """Resolve the name and return the TXT records. - - :param unicode name: Domain name being verified. - - :returns: A list of txt records, if empty the name could not be resolved - :rtype: list of unicode - - """ - try: - import dns.resolver - import dns.exception - dns_response = dns.resolver.query(name, 'TXT') - except ImportError as error: - raise ImportError("Local validation for 'dns-01' challenges requires " - "'dnspython'") - except dns.exception.DNSException as error: - logger.error("Unable to resolve %s: %s", name, str(error)) - return [] - return [txt_rec for rdata in dns_response for txt_rec in rdata.strings] diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 79a928456..0fc8fcef7 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -1,7 +1,6 @@ """Tests for acme.challenges.""" import unittest -import dns.rrset import mock import OpenSSL import requests @@ -94,16 +93,6 @@ class DNS01ResponseTest(unittest.TestCase): self.chall = DNS01(token=(b'x' * 16)) self.response = self.chall.response(KEY) - def create_txt_response(self, name, txt_records): - """ - Returns an RRSet containing the 'txt_records' as the result of a DNS - query for 'name'. - - This takes advantage of the fact that an Answer object mostly behaves - like an RRset. - """ - return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records) - def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) @@ -119,36 +108,25 @@ class DNS01ResponseTest(unittest.TestCase): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) self.response.simple_verify(self.chall, "local", key2.public_key()) - @mock.patch("acme.challenges.dns.resolver.query") - def test_simple_verify_good_validation(self, mock_dns): - mock_dns.return_value = self.create_txt_response( - self.chall.validation_domain_name("local"), - [self.chall.validation(KEY.public_key())]) + @mock.patch("acme.dns_resolver.txt_records_for_name") + def test_simple_verify_good_validation(self, mock_resolver): + mock_resolver.return_value = [self.chall.validation(KEY.public_key())] self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) - mock_dns.assert_called_once_with( - self.chall.validation_domain_name("local"), "TXT") + mock_resolver.assert_called_once_with( + self.chall.validation_domain_name("local")) - @mock.patch("acme.challenges.dns.resolver.query") - def test_simple_verify_good_validation_multiple_txts(self, mock_dns): - mock_dns.return_value = self.create_txt_response( - self.chall.validation_domain_name("local"), - ["!", self.chall.validation(KEY.public_key())]) + @mock.patch("acme.dns_resolver.txt_records_for_name") + def test_simple_verify_good_validation_multiple_txts(self, mock_resolver): + mock_resolver.return_value = ["!", self.chall.validation(KEY.public_key())] self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) - mock_dns.assert_called_once_with( - self.chall.validation_domain_name("local"), "TXT") + mock_resolver.assert_called_once_with( + self.chall.validation_domain_name("local")) - @mock.patch("acme.challenges.dns.resolver.query") + @mock.patch("acme.dns_resolver.txt_records_for_name") def test_simple_verify_bad_validation(self, mock_dns): - mock_dns.return_value = self.create_txt_response( - self.chall.validation_domain_name("local"), ["!"]) - self.assertFalse(self.response.simple_verify( - self.chall, "local", KEY.public_key())) - - @mock.patch("acme.challenges.dns.resolver.query") - def test_simple_verify_connection_error(self, mock_dns): - mock_dns.side_effect = dns.exception.DNSException + mock_dns.return_value = ["!"] self.assertFalse(self.response.simple_verify( self.chall, "local", KEY.public_key())) diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py new file mode 100644 index 000000000..04e52224e --- /dev/null +++ b/acme/acme/dns_resolver.py @@ -0,0 +1,28 @@ +"""DNS Resolver for ACME client. +Required only for local validation of 'dns-01' challenges. +""" +import logging + +import dns.resolver +import dns.exception + +logger = logging.getLogger(__name__) + +def txt_records_for_name(name): + """Resolve the name and return the TXT records. + + :param unicode name: Domain name being verified. + + :returns: A list of txt records, if empty the name could not be resolved + :rtype: list of unicode + + """ + try: + dns_response = dns.resolver.query(name, 'TXT') + except ImportError as error: + raise ImportError("Local validation for 'dns-01' challenges requires " + "'dnspython'") + except dns.exception.DNSException as error: + logger.error("Unable to resolve %s: %s", name, str(error)) + return [] + return [txt_rec for rdata in dns_response for txt_rec in rdata.strings] diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py new file mode 100644 index 000000000..d1daa2d37 --- /dev/null +++ b/acme/acme/dns_resolver_test.py @@ -0,0 +1,37 @@ +"""Tests for acme.dns_resolver.""" +import unittest + +import dns +import mock + +from acme import dns_resolver + +class TxtRecordsForNameTest(unittest.TestCase): + + def create_txt_response(self, name, txt_records): + """ + Returns an RRSet containing the 'txt_records' as the result of a DNS + query for 'name'. + + This takes advantage of the fact that an Answer object mostly behaves + like an RRset. + """ + return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records) + + @mock.patch("acme.dns_resolver.dns.resolver.query") + def test_txt_records_for_name_test_with_single_response(self, mock_dns): + mock_dns.return_value = self.create_txt_response('name', ['response']) + self.assertEqual(['response'], + dns_resolver.txt_records_for_name('name')) + + @mock.patch("acme.dns_resolver.dns.resolver.query") + def test_txt_records_for_name_with_multiple_responses(self, mock_dns): + mock_dns.return_value = self.create_txt_response( + 'name', ['response1', 'response2']) + self.assertEqual(['response1', 'response2'], + dns_resolver.txt_records_for_name('name')) + + @mock.patch("acme.dns_resolver.dns.resolver.query") + def test_txt_records_for_name_domain_not_found(self, mock_dns): + mock_dns.side_effect = dns.exception.DNSException + self.assertEquals([], dns_resolver.txt_records_for_name('name')) From a9a5e60bc59a65fb8d02f60f88946f1bafcc655c Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 01:26:32 -0500 Subject: [PATCH 014/445] Added requirements for coverage and lint. --- acme/acme/challenges.py | 2 +- acme/acme/dns_resolver_test.py | 2 +- acme/setup.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 221eb406c..22bda6445 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -245,7 +245,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse): from acme import dns_resolver txt_records = dns_resolver.txt_records_for_name( validation_domain_name) - except ImportError as error: + except ImportError: raise ImportError("Local validation for 'dns-01' challenges " "requires 'dnspython'") exists = validation in txt_records diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index d1daa2d37..2202ce4e8 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -23,7 +23,7 @@ class TxtRecordsForNameTest(unittest.TestCase): mock_dns.return_value = self.create_txt_response('name', ['response']) self.assertEqual(['response'], dns_resolver.txt_records_for_name('name')) - + @mock.patch("acme.dns_resolver.dns.resolver.query") def test_txt_records_for_name_with_multiple_responses(self, mock_dns): mock_dns.return_value = self.create_txt_response( diff --git a/acme/setup.py b/acme/setup.py index e00476117..269d9e1fd 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -74,7 +74,9 @@ setup( include_package_data=True, install_requires=install_requires, extras_require={ + 'coverage': ['dnspython'], 'docs': docs_extras, + 'lint': ['dnspython'], 'testing': testing_extras + ['dnspython'], }, entry_points={ From d2ced2de6aa31d6fe2aab4b68a7feb89aad307d1 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 01:48:12 -0500 Subject: [PATCH 015/445] Dep fixes for lint/coverage. --- acme/setup.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 269d9e1fd..e4a525605 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -35,6 +35,10 @@ if sys.version_info < (2, 7, 9): install_requires.append('ndg-httpsclient') install_requires.append('pyasn1') +dev_extras = [ + 'dnspython', +] + docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 'sphinx_rtd_theme', @@ -42,6 +46,7 @@ docs_extras = [ ] testing_extras = [ + 'dnspython', 'nose', 'tox', ] @@ -74,10 +79,9 @@ setup( include_package_data=True, install_requires=install_requires, extras_require={ - 'coverage': ['dnspython'], + 'dev': dev_extras, 'docs': docs_extras, - 'lint': ['dnspython'], - 'testing': testing_extras + ['dnspython'], + 'testing': testing_extras, }, entry_points={ 'console_scripts': [ From 52c487f462067ca810f3f3b840876306dfd1e9b1 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 02:44:19 -0500 Subject: [PATCH 016/445] Add new 'test' extras and update tox.ini accordingly. --- acme/acme/challenges_test.py | 5 ++--- acme/setup.py | 5 ++--- tox.ini | 10 +++++----- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 0fc8fcef7..ac94619b5 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -135,9 +135,8 @@ class DNS01Test(unittest.TestCase): def setUp(self): from acme.challenges import DNS01 - self.msg = DNS01( - token=jose.decode_b64jose( - 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) + self.msg = DNS01(token=jose.decode_b64jose( + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) self.jmsg = { 'type': 'dns-01', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', diff --git a/acme/setup.py b/acme/setup.py index e4a525605..3cb69ce9e 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -35,7 +35,7 @@ if sys.version_info < (2, 7, 9): install_requires.append('ndg-httpsclient') install_requires.append('pyasn1') -dev_extras = [ +dns_extras = [ 'dnspython', ] @@ -46,7 +46,6 @@ docs_extras = [ ] testing_extras = [ - 'dnspython', 'nose', 'tox', ] @@ -79,7 +78,7 @@ setup( include_package_data=True, install_requires=install_requires, extras_require={ - 'dev': dev_extras, + 'dns': dns_extras, 'docs': docs_extras, 'testing': testing_extras, }, diff --git a/tox.ini b/tox.ini index 1abe1cf39..98e185cfa 100644 --- a/tox.ini +++ b/tox.ini @@ -33,23 +33,23 @@ setenv = [testenv:py33] commands = - pip install -e acme[testing] + pip install -e acme[dev,testing] nosetests -v acme [testenv:py34] commands = - pip install -e acme[testing] + pip install -e acme[dev,testing] nosetests -v acme [testenv:py35] commands = - pip install -e acme[testing] + pip install -e acme[dev,testing] nosetests -v acme [testenv:cover] basepython = python2.7 commands = - pip install -e acme -e .[testing] -e letsencrypt-apache -e letsencrypt-nginx -e letshelp-letsencrypt + pip install -e acme[dns] -e .[testing] -e letsencrypt-apache -e letsencrypt-nginx -e letshelp-letsencrypt ./tox.cover.sh [testenv:lint] @@ -59,7 +59,7 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -e acme -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt + pip install -e acme -e .[dns,dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt ./pep8.travis.sh pylint --rcfile=.pylintrc letsencrypt pylint --rcfile=.pylintrc acme/acme From cead22f4a7d46ff921a66bd049317c78733c596b Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 02:45:20 -0500 Subject: [PATCH 017/445] Add dns env to lint/cover --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 98e185cfa..a1ed23cf7 100644 --- a/tox.ini +++ b/tox.ini @@ -59,7 +59,7 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -e acme -e .[dns,dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt + pip install -e acme[dns] -e .[dns,dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt ./pep8.travis.sh pylint --rcfile=.pylintrc letsencrypt pylint --rcfile=.pylintrc acme/acme From e61e83f7e26803b556c7e6620cd014779d1f5e36 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 02:46:29 -0500 Subject: [PATCH 018/445] tox.ini fix --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index a1ed23cf7..cdc2c2396 100644 --- a/tox.ini +++ b/tox.ini @@ -59,7 +59,7 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -e acme[dns] -e .[dns,dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt + pip install -e acme[dns] -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt ./pep8.travis.sh pylint --rcfile=.pylintrc letsencrypt pylint --rcfile=.pylintrc acme/acme From b8a9c2597c4ba6a02ed9b530c1286a1c24b7ccac Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 02:57:53 -0500 Subject: [PATCH 019/445] add dns environment to pyXX --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index cdc2c2396..4a7aea0d0 100644 --- a/tox.ini +++ b/tox.ini @@ -33,17 +33,17 @@ setenv = [testenv:py33] commands = - pip install -e acme[dev,testing] + pip install -e acme[dns,testing] nosetests -v acme [testenv:py34] commands = - pip install -e acme[dev,testing] + pip install -e acme[dns,testing] nosetests -v acme [testenv:py35] commands = - pip install -e acme[dev,testing] + pip install -e acme[dns,testing] nosetests -v acme [testenv:cover] From b73b410729e90bf0c6c1882130f6c0759675d7c3 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 02:59:25 -0500 Subject: [PATCH 020/445] Exclude import error case from coverage in dns_resolver --- acme/acme/dns_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py index 04e52224e..e28f132f1 100644 --- a/acme/acme/dns_resolver.py +++ b/acme/acme/dns_resolver.py @@ -19,7 +19,7 @@ def txt_records_for_name(name): """ try: dns_response = dns.resolver.query(name, 'TXT') - except ImportError as error: + except ImportError as error: # pragma: no cover raise ImportError("Local validation for 'dns-01' challenges requires " "'dnspython'") except dns.exception.DNSException as error: From 6bc3060fbb6a3d248654b6f97c004ce682e1e52f Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 03:11:09 -0500 Subject: [PATCH 021/445] More fixes for travis tests --- acme/acme/challenges.py | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 22bda6445..08d847627 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -245,7 +245,7 @@ class DNS01Response(KeyAuthorizationChallengeResponse): from acme import dns_resolver txt_records = dns_resolver.txt_records_for_name( validation_domain_name) - except ImportError: + except ImportError: # pragma: no cover raise ImportError("Local validation for 'dns-01' challenges " "requires 'dnspython'") exists = validation in txt_records diff --git a/tox.ini b/tox.ini index 4a7aea0d0..c0a819b5e 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ envlist = py26,py27,py33,py34,py35,cover,lint # packages installed separately to ensure that dowstream deps problems # are detected, c.f. #1002 commands = - pip install -e acme[testing] + pip install -e acme[dns,testing] nosetests -v acme pip install -e .[testing] nosetests -v letsencrypt From 02a493011ef039ca6ec1d52d9c87d4317afe0d12 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Wed, 6 Jan 2016 22:56:59 -0500 Subject: [PATCH 022/445] Remove superfluous except: and change Exception returned if dnspython is not available. --- acme/acme/challenges.py | 4 ++-- acme/acme/dns_resolver.py | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 08d847627..3a78e403a 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -246,8 +246,8 @@ class DNS01Response(KeyAuthorizationChallengeResponse): txt_records = dns_resolver.txt_records_for_name( validation_domain_name) except ImportError: # pragma: no cover - raise ImportError("Local validation for 'dns-01' challenges " - "requires 'dnspython'") + raise errors.Error("Local validation for 'dns-01' challenges " + "requires 'dnspython'") exists = validation in txt_records if not exists: logger.debug("Key authorization from response (%r) doesn't match " diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py index e28f132f1..3572ee277 100644 --- a/acme/acme/dns_resolver.py +++ b/acme/acme/dns_resolver.py @@ -19,9 +19,6 @@ def txt_records_for_name(name): """ try: dns_response = dns.resolver.query(name, 'TXT') - except ImportError as error: # pragma: no cover - raise ImportError("Local validation for 'dns-01' challenges requires " - "'dnspython'") except dns.exception.DNSException as error: logger.error("Unable to resolve %s: %s", name, str(error)) return [] From 446994e8ef11a860ac94a52751bc51a9fded6b04 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 9 Jan 2016 14:58:19 -0500 Subject: [PATCH 023/445] Limit length of try block. --- acme/acme/challenges.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 3a78e403a..493d230d8 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -243,11 +243,10 @@ class DNS01Response(KeyAuthorizationChallengeResponse): try: from acme import dns_resolver - txt_records = dns_resolver.txt_records_for_name( - validation_domain_name) except ImportError: # pragma: no cover raise errors.Error("Local validation for 'dns-01' challenges " "requires 'dnspython'") + txt_records = dns_resolver.txt_records_for_name(validation_domain_name) exists = validation in txt_records if not exists: logger.debug("Key authorization from response (%r) doesn't match " From 56154f1301725dff575e22011a81ca18d5729307 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 9 Jan 2016 16:43:43 -0500 Subject: [PATCH 024/445] - Use dnspython3 fir py3X environments. - Fix encoding for simple_verify. --- acme/acme/challenges.py | 4 ++-- acme/setup.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 493d230d8..3fa259b13 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -271,8 +271,8 @@ class DNS01(KeyAuthorizationChallenge): :rtype: unicode """ - return jose.b64encode(hashlib.sha256( - self.key_authorization(account_key)).digest()) + return jose.b64encode(hashlib.sha256(self.key_authorization( + account_key).encode("utf-8")).digest()).decode() def validation_domain_name(self, name): """Domain name for TXT validation record. diff --git a/acme/setup.py b/acme/setup.py index c54dc42e6..df5cfba0e 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -35,9 +35,14 @@ if sys.version_info < (2, 7, 9): install_requires.append('ndg-httpsclient') install_requires.append('pyasn1') -dns_extras = [ - 'dnspython', -] +if sys.version_info < (3, 0): + dns_extras = [ + 'dnspython', + ] +else: + dns_extras = [ + 'dnspython3', + ] docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags From d842f268e5414f418e69913f6ac4fadffb327fb1 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 9 Jan 2016 17:07:20 -0500 Subject: [PATCH 025/445] - Use dnspython3 fir py3X environments. - Fix encoding for simple_verify. --- acme/acme/challenges.py | 4 ++-- acme/setup.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 493d230d8..3fa259b13 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -271,8 +271,8 @@ class DNS01(KeyAuthorizationChallenge): :rtype: unicode """ - return jose.b64encode(hashlib.sha256( - self.key_authorization(account_key)).digest()) + return jose.b64encode(hashlib.sha256(self.key_authorization( + account_key).encode("utf-8")).digest()).decode() def validation_domain_name(self, name): """Domain name for TXT validation record. diff --git a/acme/setup.py b/acme/setup.py index c54dc42e6..df5cfba0e 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -35,9 +35,14 @@ if sys.version_info < (2, 7, 9): install_requires.append('ndg-httpsclient') install_requires.append('pyasn1') -dns_extras = [ - 'dnspython', -] +if sys.version_info < (3, 0): + dns_extras = [ + 'dnspython', + ] +else: + dns_extras = [ + 'dnspython3', + ] docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags From b7c00a889a607add72823aa20ccf4874a585a8eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20B=C3=B6gershausen?= Date: Sun, 10 Jan 2016 18:31:49 +0100 Subject: [PATCH 026/445] .gitattributes: EOL=LF overrides auto The line * text=auto eol=lf in .gitattributes does not work as expected: setting eol=lf overrides text=auto, and works as * eol=lf or * text eol=lf This is not what is intended, as binary files like *.gz go through a CRLF -> LF conversion at commit time. Use * crlf=auto instead (which is even understood by older Git versions, which are still in use) --- .gitattributes | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 5eee84cce..2a18bd183 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,15 @@ -* text=auto eol=lf +#Default, normalize CRLF into LF in non-binary files +# Files identified as binary by Git are not changed +* crlf=auto # special files +*.sh crlf=input +*.py crlf=input + *.bat text eol=crlf + +*.der binary +*.gz binary *.jpeg binary *.jpg binary *.png binary From f2b52bd830457200474e98394b8aff0315e9d51f Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 12:58:29 -0500 Subject: [PATCH 027/445] Fix dcumentation --- acme/acme/challenges.py | 1 - 1 file changed, 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 3fa259b13..a5b142c5b 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -226,7 +226,6 @@ class DNS01Response(KeyAuthorizationChallengeResponse): :param unicode domain: Domain name being verified. :param account_public_key: Public key for the key pair being authorized. - :param JWK account_public_key: :returns: ``True`` iff validation with the TXT records resolved from a DNS server is successful. From 49c40e7a584d1d8faeac4436c6c7787e44a12b6d Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 13:00:38 -0500 Subject: [PATCH 028/445] Skip dns_resolver tests if dnspython is not available. --- acme/acme/dns_resolver_test.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 2202ce4e8..0d5cc2543 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -1,11 +1,17 @@ """Tests for acme.dns_resolver.""" import unittest -import dns import mock +try: + import dns +except ImportError: + dns = None + from acme import dns_resolver +@unittest.skipIf(dns is None, + "dnspython is not available, skipping dns_resolver tests") class TxtRecordsForNameTest(unittest.TestCase): def create_txt_response(self, name, txt_records): From cfe56cbd925a4c90a051355f4f891ee8f15308d0 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 17:00:14 -0500 Subject: [PATCH 029/445] 2.6 compatible skipping of tests. --- acme/acme/dns_resolver_test.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 0d5cc2543..54f074a92 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -1,17 +1,15 @@ """Tests for acme.dns_resolver.""" import unittest - import mock -try: - import dns -except ImportError: - dns = None - from acme import dns_resolver -@unittest.skipIf(dns is None, - "dnspython is not available, skipping dns_resolver tests") +try: + import dns +except ImportError: # pragma: no cover + dns = None + + class TxtRecordsForNameTest(unittest.TestCase): def create_txt_response(self, name, txt_records): @@ -41,3 +39,9 @@ class TxtRecordsForNameTest(unittest.TestCase): def test_txt_records_for_name_domain_not_found(self, mock_dns): mock_dns.side_effect = dns.exception.DNSException self.assertEquals([], dns_resolver.txt_records_for_name('name')) + + def run(self, result=None): + if dns is None: + print self, "... SKIPPING, no dnspython available" + return + super(TxtRecordsForNameTest, self).run(result) From 0010610a4a038fd592c0544d02fd0f36a8ff3495 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 17:06:03 -0500 Subject: [PATCH 030/445] py3X fix --- acme/acme/dns_resolver_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 54f074a92..8e92cd4a3 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -42,6 +42,6 @@ class TxtRecordsForNameTest(unittest.TestCase): def run(self, result=None): if dns is None: - print self, "... SKIPPING, no dnspython available" + print(self, "... SKIPPING, no dnspython available") return super(TxtRecordsForNameTest, self).run(result) From 2d8de74f4abd659cad518ec5d74d8613fe9830ac Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 17:13:25 -0500 Subject: [PATCH 031/445] pcoverage fix --- acme/acme/dns_resolver_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 8e92cd4a3..770a95a77 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -7,7 +7,7 @@ from acme import dns_resolver try: import dns except ImportError: # pragma: no cover - dns = None + dns = Nones class TxtRecordsForNameTest(unittest.TestCase): @@ -41,7 +41,7 @@ class TxtRecordsForNameTest(unittest.TestCase): self.assertEquals([], dns_resolver.txt_records_for_name('name')) def run(self, result=None): - if dns is None: + if dns is None: # pragma: no cover print(self, "... SKIPPING, no dnspython available") return super(TxtRecordsForNameTest, self).run(result) From 1ff121b616c8d599549be19a07bd45fb8429fc49 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 18:08:16 -0500 Subject: [PATCH 032/445] pcoverage fix --- acme/acme/dns_resolver_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 770a95a77..5d160fc07 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -7,7 +7,7 @@ from acme import dns_resolver try: import dns except ImportError: # pragma: no cover - dns = Nones + dns = None class TxtRecordsForNameTest(unittest.TestCase): From 9179276cb982945f6d6083b26d7a55d0112894e9 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 20:59:11 -0500 Subject: [PATCH 033/445] Modify dns_resolver_test to skip tests if dnspython is not available. --- acme/acme/dns_resolver_test.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 2202ce4e8..5d160fc07 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -1,11 +1,15 @@ """Tests for acme.dns_resolver.""" import unittest - -import dns import mock from acme import dns_resolver +try: + import dns +except ImportError: # pragma: no cover + dns = None + + class TxtRecordsForNameTest(unittest.TestCase): def create_txt_response(self, name, txt_records): @@ -35,3 +39,9 @@ class TxtRecordsForNameTest(unittest.TestCase): def test_txt_records_for_name_domain_not_found(self, mock_dns): mock_dns.side_effect = dns.exception.DNSException self.assertEquals([], dns_resolver.txt_records_for_name('name')) + + def run(self, result=None): + if dns is None: # pragma: no cover + print(self, "... SKIPPING, no dnspython available") + return + super(TxtRecordsForNameTest, self).run(result) From 05a61c181b277d4a3f94abe038da0fda549faeda Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sun, 10 Jan 2016 21:42:20 -0500 Subject: [PATCH 034/445] Lint fixes. --- acme/acme/challenges_test.py | 2 +- acme/acme/dns_resolver_test.py | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index ac94619b5..b32bebed0 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -114,7 +114,7 @@ class DNS01ResponseTest(unittest.TestCase): self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_resolver.assert_called_once_with( - self.chall.validation_domain_name("local")) + self.chall.validation_domain_name("local")) @mock.patch("acme.dns_resolver.txt_records_for_name") def test_simple_verify_good_validation_multiple_txts(self, mock_resolver): diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 5d160fc07..39645c492 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -9,28 +9,28 @@ try: except ImportError: # pragma: no cover dns = None +def create_txt_response(name, txt_records): + """ + Returns an RRSet containing the 'txt_records' as the result of a DNS + query for 'name'. + + This takes advantage of the fact that an Answer object mostly behaves + like an RRset. + """ + return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records) + class TxtRecordsForNameTest(unittest.TestCase): - def create_txt_response(self, name, txt_records): - """ - Returns an RRSet containing the 'txt_records' as the result of a DNS - query for 'name'. - - This takes advantage of the fact that an Answer object mostly behaves - like an RRset. - """ - return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records) - @mock.patch("acme.dns_resolver.dns.resolver.query") - def test_txt_records_for_name_test_with_single_response(self, mock_dns): - mock_dns.return_value = self.create_txt_response('name', ['response']) + def test_txt_records_for_name_with_single_response(self, mock_dns): + mock_dns.return_value = create_txt_response('name', ['response']) self.assertEqual(['response'], dns_resolver.txt_records_for_name('name')) @mock.patch("acme.dns_resolver.dns.resolver.query") def test_txt_records_for_name_with_multiple_responses(self, mock_dns): - mock_dns.return_value = self.create_txt_response( + mock_dns.return_value = create_txt_response( 'name', ['response1', 'response2']) self.assertEqual(['response1', 'response2'], dns_resolver.txt_records_for_name('name')) From c15581bcfd4b9ffe8d1e1ceb03e316c337691524 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Thu, 14 Jan 2016 23:37:05 -0500 Subject: [PATCH 035/445] Fix lint problems. --- acme/acme/challenges_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 39f7449f2..af4b81f80 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -119,7 +119,8 @@ class DNS01ResponseTest(unittest.TestCase): @mock.patch("acme.dns_resolver.txt_records_for_name") def test_simple_verify_good_validation_multiple_txts(self, mock_resolver): - mock_resolver.return_value = ["!", self.chall.validation(KEY.public_key())] + mock_resolver.return_value = [ + "!", self.chall.validation(KEY.public_key())] self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_resolver.assert_called_once_with( From 86ab9a2afc6de4e51301ee3dc278a17e7ea06413 Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Thu, 21 Jan 2016 19:08:38 -0500 Subject: [PATCH 036/445] Make ssl includes be an array of directives instead of an include of a file. This will be the first step to filter out the things that would conflict with earlier declarations --- .../letsencrypt_nginx/configurator.py | 3 +-- letsencrypt-nginx/letsencrypt_nginx/parser.py | 18 +++++++++++++++++- .../tests/configurator_test.py | 13 +++++++------ .../letsencrypt_nginx/tests/parser_test.py | 11 +++++++++++ .../letsencrypt_nginx/tls_sni_01.py | 4 ++-- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index efa7e08b4..918ba53c2 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -332,8 +332,7 @@ class NginxConfigurator(common.Plugin): snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)], ['ssl_certificate', snakeoil_cert], - ['ssl_certificate_key', snakeoil_key], - ['include', self.parser.loc["ssl_options"]]] + ['ssl_certificate_key', snakeoil_key]] + self.parser.loc["ssl_options"] self.parser.add_server_directives( vhost.filep, vhost.names, ssl_block, replace=False) vhost.ssl = True diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 3b1dd049e..935bf40dd 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -169,6 +169,18 @@ class NginxParser(object): logger.debug("Could not parse file: %s", item) return trees + def _parse_ssl_options(self, ssl_options): + if ssl_options is not None: + try: + with open(ssl_options) as _file: + return nginxparser.load(_file) + except IOError: + logger.debug("Could not open file: %s", item) + except pyparsing.ParseException: + logger.debug("Could not parse file: %s", item) + else: + return [] + def _set_locations(self, ssl_options): """Set default location for directives. @@ -188,7 +200,7 @@ class NginxParser(object): name = default return {"root": root, "default": default, "listen": listen, - "name": name, "ssl_options": ssl_options} + "name": name, "ssl_options": self._parse_ssl_options(ssl_options)} def _find_config_root(self): """Find the Nginx Configuration Root file.""" @@ -503,6 +515,10 @@ def _add_directive(block, directive, replace): See _add_directives for more documentation. """ + if directive[0] == '#': + block.append(directive) + return + location = -1 # Find the index of a config line where the name of the directive matches # the name of the directive we want to add. diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index 4fce33079..f35dcdfc0 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -216,9 +216,9 @@ class NginxConfiguratorTest(util.NginxTest): ['listen', '5001 ssl'], ['ssl_certificate', 'example/fullchain.pem'], - ['ssl_certificate_key', 'example/key.pem'], - ['include', self.config.parser.loc["ssl_options"]] - ]]], + ['ssl_certificate_key', 'example/key.pem']] + + util.filter_comments(self.config.parser.loc["ssl_options"]) + ]], parsed_example_conf) self.assertEqual([['server_name', 'somename alias another.alias']], parsed_server_conf) @@ -234,8 +234,9 @@ class NginxConfiguratorTest(util.NginxTest): ['index', 'index.html index.htm']]], ['listen', '5001 ssl'], ['ssl_certificate', '/etc/nginx/fullchain.pem'], - ['ssl_certificate_key', '/etc/nginx/key.pem'], - ['include', self.config.parser.loc["ssl_options"]]]], + ['ssl_certificate_key', '/etc/nginx/key.pem']]+ + util.filter_comments(self.config.parser.loc["ssl_options"]) + ], 2)) def test_get_all_certs_keys(self): @@ -394,6 +395,6 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[example_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) - + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py index b64f1dee3..1190a2326 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py @@ -249,5 +249,16 @@ class NginxParserTest(util.NginxTest): ]) self.assertTrue(server['ssl']) + def test_ssl_options_should_be_parsed_ssl_directives(self): + nparser = parser.NginxParser(self.config_path, self.ssl_options) + self.assertEqual(nparser.loc["ssl_options"], + [['ssl_session_cache', 'shared:SSL:1m'], + ['ssl_session_timeout', '1440m'], + ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], + ['ssl_prefer_server_ciphers', 'on'], + ['#', ' Using list of ciphers from "Bulletproof SSL and TLS"'], + ['ssl_ciphers', '"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"'] + ]) + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py b/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py index e59281c4c..5afc950ad 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py @@ -146,7 +146,6 @@ class NginxTlsSni01(common.TLSSNI01): block.extend([['server_name', achall.response(achall.account_key).z_domain], - ['include', self.configurator.parser.loc["ssl_options"]], # access and error logs necessary for # integration testing (non-root) ['access_log', os.path.join( @@ -155,6 +154,7 @@ class NginxTlsSni01(common.TLSSNI01): self.configurator.config.work_dir, 'error.log')], ['ssl_certificate', self.get_cert_path(achall)], ['ssl_certificate_key', self.get_key_path(achall)], - [['location', '/'], [['root', document_root]]]]) + [['location', '/'], [['root', document_root]]]] + + self.configurator.parser.loc["ssl_options"]) return [['server'], block] From 25674b4d559a630e15ac2941e410bc4ca04586e6 Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 12:12:29 -0500 Subject: [PATCH 037/445] Change name of session cache in order to minimize risk of conflict --- letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf b/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf index f0081c1fc..3faab8818 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf +++ b/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf @@ -1,4 +1,4 @@ -ssl_session_cache shared:SSL:1m; +ssl_session_cache shared:le_nginx_SSL:1m; ssl_session_timeout 1440m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; From d53da41f007e7f8750a528554bf4000b3d7c6a03 Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 15:03:51 -0500 Subject: [PATCH 038/445] Stupid mistake, forgot to change the test --- letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py index 1190a2326..3fc6a214e 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py @@ -252,7 +252,7 @@ class NginxParserTest(util.NginxTest): def test_ssl_options_should_be_parsed_ssl_directives(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) self.assertEqual(nparser.loc["ssl_options"], - [['ssl_session_cache', 'shared:SSL:1m'], + [['ssl_session_cache', 'shared:le_nginx_SSL:1m'], ['ssl_session_timeout', '1440m'], ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], ['ssl_prefer_server_ciphers', 'on'], From 593e220353f720282e112809fddd954e575c6779 Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 16:28:05 -0500 Subject: [PATCH 039/445] A final small fix, hopefully --- letsencrypt-nginx/letsencrypt_nginx/parser.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 935bf40dd..bd9bebe08 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -175,11 +175,10 @@ class NginxParser(object): with open(ssl_options) as _file: return nginxparser.load(_file) except IOError: - logger.debug("Could not open file: %s", item) + logger.debug("Could not open file: %s", ssl_options) except pyparsing.ParseException: - logger.debug("Could not parse file: %s", item) - else: - return [] + logger.debug("Could not parse file: %s", ssl_options) + return [] def _set_locations(self, ssl_options): """Set default location for directives. From 578db8b8406382da21db6731f4c63dbbeb9a4b2d Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Thu, 21 Jan 2016 19:08:38 -0500 Subject: [PATCH 040/445] Make ssl includes be an array of directives instead of an include of a file. This will be the first step to filter out the things that would conflict with earlier declarations --- .../letsencrypt_nginx/configurator.py | 3 +-- letsencrypt-nginx/letsencrypt_nginx/parser.py | 18 +++++++++++++++++- .../tests/configurator_test.py | 13 +++++++------ .../letsencrypt_nginx/tests/parser_test.py | 11 +++++++++++ .../letsencrypt_nginx/tls_sni_01.py | 4 ++-- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index efa7e08b4..918ba53c2 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -332,8 +332,7 @@ class NginxConfigurator(common.Plugin): snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)], ['ssl_certificate', snakeoil_cert], - ['ssl_certificate_key', snakeoil_key], - ['include', self.parser.loc["ssl_options"]]] + ['ssl_certificate_key', snakeoil_key]] + self.parser.loc["ssl_options"] self.parser.add_server_directives( vhost.filep, vhost.names, ssl_block, replace=False) vhost.ssl = True diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 3b1dd049e..935bf40dd 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -169,6 +169,18 @@ class NginxParser(object): logger.debug("Could not parse file: %s", item) return trees + def _parse_ssl_options(self, ssl_options): + if ssl_options is not None: + try: + with open(ssl_options) as _file: + return nginxparser.load(_file) + except IOError: + logger.debug("Could not open file: %s", item) + except pyparsing.ParseException: + logger.debug("Could not parse file: %s", item) + else: + return [] + def _set_locations(self, ssl_options): """Set default location for directives. @@ -188,7 +200,7 @@ class NginxParser(object): name = default return {"root": root, "default": default, "listen": listen, - "name": name, "ssl_options": ssl_options} + "name": name, "ssl_options": self._parse_ssl_options(ssl_options)} def _find_config_root(self): """Find the Nginx Configuration Root file.""" @@ -503,6 +515,10 @@ def _add_directive(block, directive, replace): See _add_directives for more documentation. """ + if directive[0] == '#': + block.append(directive) + return + location = -1 # Find the index of a config line where the name of the directive matches # the name of the directive we want to add. diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index 4fce33079..f35dcdfc0 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -216,9 +216,9 @@ class NginxConfiguratorTest(util.NginxTest): ['listen', '5001 ssl'], ['ssl_certificate', 'example/fullchain.pem'], - ['ssl_certificate_key', 'example/key.pem'], - ['include', self.config.parser.loc["ssl_options"]] - ]]], + ['ssl_certificate_key', 'example/key.pem']] + + util.filter_comments(self.config.parser.loc["ssl_options"]) + ]], parsed_example_conf) self.assertEqual([['server_name', 'somename alias another.alias']], parsed_server_conf) @@ -234,8 +234,9 @@ class NginxConfiguratorTest(util.NginxTest): ['index', 'index.html index.htm']]], ['listen', '5001 ssl'], ['ssl_certificate', '/etc/nginx/fullchain.pem'], - ['ssl_certificate_key', '/etc/nginx/key.pem'], - ['include', self.config.parser.loc["ssl_options"]]]], + ['ssl_certificate_key', '/etc/nginx/key.pem']]+ + util.filter_comments(self.config.parser.loc["ssl_options"]) + ], 2)) def test_get_all_certs_keys(self): @@ -394,6 +395,6 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[example_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) - + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py index b64f1dee3..1190a2326 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py @@ -249,5 +249,16 @@ class NginxParserTest(util.NginxTest): ]) self.assertTrue(server['ssl']) + def test_ssl_options_should_be_parsed_ssl_directives(self): + nparser = parser.NginxParser(self.config_path, self.ssl_options) + self.assertEqual(nparser.loc["ssl_options"], + [['ssl_session_cache', 'shared:SSL:1m'], + ['ssl_session_timeout', '1440m'], + ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], + ['ssl_prefer_server_ciphers', 'on'], + ['#', ' Using list of ciphers from "Bulletproof SSL and TLS"'], + ['ssl_ciphers', '"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"'] + ]) + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py b/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py index e59281c4c..5afc950ad 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tls_sni_01.py @@ -146,7 +146,6 @@ class NginxTlsSni01(common.TLSSNI01): block.extend([['server_name', achall.response(achall.account_key).z_domain], - ['include', self.configurator.parser.loc["ssl_options"]], # access and error logs necessary for # integration testing (non-root) ['access_log', os.path.join( @@ -155,6 +154,7 @@ class NginxTlsSni01(common.TLSSNI01): self.configurator.config.work_dir, 'error.log')], ['ssl_certificate', self.get_cert_path(achall)], ['ssl_certificate_key', self.get_key_path(achall)], - [['location', '/'], [['root', document_root]]]]) + [['location', '/'], [['root', document_root]]]] + + self.configurator.parser.loc["ssl_options"]) return [['server'], block] From 067e51170296fae8f908f60e357d99cbe62a09bc Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 12:12:29 -0500 Subject: [PATCH 041/445] Change name of session cache in order to minimize risk of conflict --- letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf b/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf index f0081c1fc..3faab8818 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf +++ b/letsencrypt-nginx/letsencrypt_nginx/options-ssl-nginx.conf @@ -1,4 +1,4 @@ -ssl_session_cache shared:SSL:1m; +ssl_session_cache shared:le_nginx_SSL:1m; ssl_session_timeout 1440m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; From 613250e1b26f4c28354932340830eb0b09e5e3cb Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 15:03:51 -0500 Subject: [PATCH 042/445] Stupid mistake, forgot to change the test --- letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py index 1190a2326..3fc6a214e 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py @@ -252,7 +252,7 @@ class NginxParserTest(util.NginxTest): def test_ssl_options_should_be_parsed_ssl_directives(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) self.assertEqual(nparser.loc["ssl_options"], - [['ssl_session_cache', 'shared:SSL:1m'], + [['ssl_session_cache', 'shared:le_nginx_SSL:1m'], ['ssl_session_timeout', '1440m'], ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], ['ssl_prefer_server_ciphers', 'on'], From f4a130eab3529fb8860669ecce3b5d2fd8393dec Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Fri, 22 Jan 2016 16:28:05 -0500 Subject: [PATCH 043/445] A final small fix, hopefully --- letsencrypt-nginx/letsencrypt_nginx/parser.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 935bf40dd..bd9bebe08 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -175,11 +175,10 @@ class NginxParser(object): with open(ssl_options) as _file: return nginxparser.load(_file) except IOError: - logger.debug("Could not open file: %s", item) + logger.debug("Could not open file: %s", ssl_options) except pyparsing.ParseException: - logger.debug("Could not parse file: %s", item) - else: - return [] + logger.debug("Could not parse file: %s", ssl_options) + return [] def _set_locations(self, ssl_options): """Set default location for directives. From 2b097b0fb2c65939e310d3dd271f1d65a11f8c9e Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Mon, 25 Jan 2016 11:39:46 -0500 Subject: [PATCH 044/445] Fix lint issues --- letsencrypt-nginx/letsencrypt_nginx/parser.py | 2 +- .../letsencrypt_nginx/tests/configurator_test.py | 2 +- .../letsencrypt_nginx/tests/parser_test.py | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index bd9bebe08..1cf805eef 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -179,7 +179,7 @@ class NginxParser(object): except pyparsing.ParseException: logger.debug("Could not parse file: %s", ssl_options) return [] - + def _set_locations(self, ssl_options): """Set default location for directives. diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index f35dcdfc0..c91372651 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -395,6 +395,6 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[example_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) - + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py index 3fc6a214e..d66206a57 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/parser_test.py @@ -257,8 +257,15 @@ class NginxParserTest(util.NginxTest): ['ssl_protocols', 'TLSv1 TLSv1.1 TLSv1.2'], ['ssl_prefer_server_ciphers', 'on'], ['#', ' Using list of ciphers from "Bulletproof SSL and TLS"'], - ['ssl_ciphers', '"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"'] + ['ssl_ciphers', '"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-'+ + 'AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256'+ + '-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384'+ + ' ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384'+ + ' ECDHE-RSA-AES128-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-'+ + 'AES256-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM'+ + '-SHA384 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA DHE-RSA-'+ + 'AES128-SHA256 DHE-RSA-AES256-SHA256 EDH-RSA-DES-CBC3-SHA"'] ]) - + if __name__ == "__main__": unittest.main() # pragma: no cover From 6ffa14c632775e361fdce96badca09c5e195f93d Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Mon, 25 Jan 2016 14:22:03 -0500 Subject: [PATCH 045/445] Bad merge managed to introduce whitespace again. Sigh. --- letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index f35dcdfc0..c91372651 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -395,6 +395,6 @@ class NginxConfiguratorTest(util.NginxTest): generated_conf = self.config.parser.parsed[example_conf] self.assertTrue(util.contains_at_depth(generated_conf, expected, 2)) - + if __name__ == "__main__": unittest.main() # pragma: no cover From 7c3271545fe48de9c67b2781ab1fd58328e4e095 Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Sat, 13 Feb 2016 01:05:35 -0500 Subject: [PATCH 046/445] Do not log an error when getting NXDOMAIN. --- acme/acme/dns_resolver.py | 4 +++- acme/acme/dns_resolver_test.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py index 3572ee277..badbf486a 100644 --- a/acme/acme/dns_resolver.py +++ b/acme/acme/dns_resolver.py @@ -19,7 +19,9 @@ def txt_records_for_name(name): """ try: dns_response = dns.resolver.query(name, 'TXT') + except dns.resolver.NXDOMAIN as error: + return [] except dns.exception.DNSException as error: - logger.error("Unable to resolve %s: %s", name, str(error)) + logger.error("Error resolving %s: %s", name, str(error)) return [] return [txt_rec for rdata in dns_response for txt_rec in rdata.strings] diff --git a/acme/acme/dns_resolver_test.py b/acme/acme/dns_resolver_test.py index 80481343d..53fc0cc77 100644 --- a/acme/acme/dns_resolver_test.py +++ b/acme/acme/dns_resolver_test.py @@ -38,6 +38,11 @@ class TxtRecordsForNameTest(unittest.TestCase): @mock.patch("acme.dns_resolver.dns.resolver.query") def test_txt_records_for_name_domain_not_found(self, mock_dns): + mock_dns.side_effect = dns.resolver.NXDOMAIN + self.assertEquals([], dns_resolver.txt_records_for_name('name')) + + @mock.patch("acme.dns_resolver.dns.resolver.query") + def test_txt_records_for_name_domain_other_error(self, mock_dns): mock_dns.side_effect = dns.exception.DNSException self.assertEquals([], dns_resolver.txt_records_for_name('name')) From ac2ec3457df6b699916864f26a89a1cc6fcadd9d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 5 Apr 2016 11:33:33 -0700 Subject: [PATCH 047/445] NcursesDisplay.menu: treat ESC as cancel Currently it will fire a weird traceback like: File "/home/ubuntu/letsencrypt/letsencrypt/plugins/selection.py", line 113, in choose_plugin code, index = disp.menu(question, opts, help_label="More Info") File "/home/ubuntu/letsencrypt/letsencrypt/display/util.py", line 129, in menu return code, int(index) - 1 ValueError: invalid literal for int() with base 10: '' --- letsencrypt/display/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 20c6be156..40dd00f8b 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -123,7 +123,7 @@ class NcursesDisplay(object): # pylint: disable=star-args code, index = self.dialog.menu(message, **menu_options) - if code == CANCEL: + if code == CANCEL or index == "": return code, -1 return code, int(index) - 1 From 9396e92a966cbc07c0a69b7148f760b4337823dd Mon Sep 17 00:00:00 2001 From: Wilfried Teiken Date: Mon, 25 Apr 2016 00:46:45 -0400 Subject: [PATCH 048/445] Fix lint issues. --- acme/acme/challenges.py | 1 - acme/acme/dns_resolver.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index d27719449..f611578dd 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -599,4 +599,3 @@ class DNSResponse(ChallengeResponse): """ return chall.check_validation(self.validation, account_public_key) - diff --git a/acme/acme/dns_resolver.py b/acme/acme/dns_resolver.py index badbf486a..15638e5d0 100644 --- a/acme/acme/dns_resolver.py +++ b/acme/acme/dns_resolver.py @@ -8,6 +8,7 @@ import dns.exception logger = logging.getLogger(__name__) + def txt_records_for_name(name): """Resolve the name and return the TXT records. From ecbff9e60d7913be893f1931ccd5e19fe3ab00f9 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 9 May 2016 14:40:03 -0700 Subject: [PATCH 049/445] migrate packaging info --- docs/packaging.rst | 77 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/docs/packaging.rst b/docs/packaging.rst index 5f09b65fa..c0baf6232 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -2,5 +2,78 @@ Packaging Guide =============== -Documentation can be found at -https://github.com/letsencrypt/letsencrypt/wiki/Packaging. +## Releases + +We release packages and upload them to PyPI (wheels and source tarballs). + +- https://pypi.python.org/pypi/acme +- https://pypi.python.org/pypi/letsencrypt +- https://pypi.python.org/pypi/letsencrypt-apache +- https://pypi.python.org/pypi/letsencrypt-nginx +- https://pypi.python.org/pypi/letshelp-letsencrypt + +The following scripts are used in the process: + +- https://github.com/letsencrypt/letsencrypt/blob/master/tools/release.sh +- https://gist.github.com/kuba/b9a3a2ca3bd35b8368ef + +We currently version as `0.0.0.devYYYYMMDD`, and will change at GA time to the following scheme: +- `0.1.0` +- `0.2.0dev` for developement in `master` +- `0.2.0` (only temporarily in `master`) +- ... + +Tracking issue for non-dev release scripts: https://github.com/letsencrypt/letsencrypt/issues/1185 + +## Notes for package maintainers + +0. Please use our releases, not `master`! + +1. Do not package `letsencrypt-compatibility-test` - it's only used internally. + +2. `letsencrypt-renewer` should be added to crontab... but it currently doesn't work well + +3. `letsencrypt.client` provides developer API so it should be possible to `import letsencrypt.client` when the package is installed (`letsencrypt` vs `python-letsencrypt` debate for Debian). + +4. `jws` is an internal script for `acme` module and it doesn't have to be packaged - it's mostly for debugging: you can use it as `echo foo | jws sign | jws verify`. + +5. Do get in touch with us. We are happy to make any changes that will make packaging easier. If you need to apply some patches don't do it downstream - make a PR here. + +## Already ongoing efforts + + + +### Arch + +From PyPI: +- https://www.archlinux.org/packages/community/any/python2-acme +- https://www.archlinux.org/packages/community/any/letsencrypt +- https://www.archlinux.org/packages/community/any/letsencrypt-apache +- https://www.archlinux.org/packages/community/any/letsencrypt-nginx +- https://www.archlinux.org/packages/community/any/letshelp-letsencrypt + +From `master`: https://aur.archlinux.org/packages/letsencrypt-git + +### Debian (and its derivatives, including Ubuntu) + +https://alioth.debian.org/projects/letsencrypt/ + +### Fedora +In Fedora 23+. + +- https://admin.fedoraproject.org/pkgdb/package/letsencrypt/ +- https://admin.fedoraproject.org/pkgdb/package/python-acme/ +- https://bugzilla.redhat.com/show_bug.cgi?id=1287193 (review request, closed) + +### FreeBSD + +https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=203405 + +### GNU Guix +- https://www.gnu.org/software/guix/package-list.html#letsencrypt + +### OpenBSD + +- http://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/security/letsencrypt/ +- https://github.com/letsencrypt/letsencrypt/pull/1175 +- https://github.com/letsencrypt/letsencrypt/issues/1174 From 05131da32e6f88101c300aebfb7e4f3e7eec4263 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 9 May 2016 14:44:26 -0700 Subject: [PATCH 050/445] make code blocks code blocks --- docs/packaging.rst | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/packaging.rst b/docs/packaging.rst index c0baf6232..8ec7d7fca 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -2,7 +2,8 @@ Packaging Guide =============== -## Releases +Releases +======== We release packages and upload them to PyPI (wheels and source tarballs). @@ -17,25 +18,25 @@ The following scripts are used in the process: - https://github.com/letsencrypt/letsencrypt/blob/master/tools/release.sh - https://gist.github.com/kuba/b9a3a2ca3bd35b8368ef -We currently version as `0.0.0.devYYYYMMDD`, and will change at GA time to the following scheme: -- `0.1.0` -- `0.2.0dev` for developement in `master` -- `0.2.0` (only temporarily in `master`) +We currently version as ``0.0.0.devYYYYMMDD``, and will change at GA time to the following scheme: +- ``0.1.0`` +- ``0.2.0dev`` for developement in ``master`` +- ``0.2.0`` (only temporarily in ``master``) - ... Tracking issue for non-dev release scripts: https://github.com/letsencrypt/letsencrypt/issues/1185 ## Notes for package maintainers -0. Please use our releases, not `master`! +0. Please use our releases, not ``master``! -1. Do not package `letsencrypt-compatibility-test` - it's only used internally. +1. Do not package ``letsencrypt-compatibility-test`` - it's only used internally. -2. `letsencrypt-renewer` should be added to crontab... but it currently doesn't work well +2. ``letsencrypt-renewer`` should be added to crontab... but it currently doesn't work well -3. `letsencrypt.client` provides developer API so it should be possible to `import letsencrypt.client` when the package is installed (`letsencrypt` vs `python-letsencrypt` debate for Debian). +3. ``letsencrypt.client`` provides developer API so it should be possible to ``import letsencrypt.client`` when the package is installed (``letsencrypt`` vs ``python-letsencrypt`` debate for Debian). -4. `jws` is an internal script for `acme` module and it doesn't have to be packaged - it's mostly for debugging: you can use it as `echo foo | jws sign | jws verify`. +4. ``jws`` is an internal script for ``acme`` module and it doesn't have to be packaged - it's mostly for debugging: you can use it as ``echo foo | jws sign | jws verify``. 5. Do get in touch with us. We are happy to make any changes that will make packaging easier. If you need to apply some patches don't do it downstream - make a PR here. @@ -52,7 +53,7 @@ From PyPI: - https://www.archlinux.org/packages/community/any/letsencrypt-nginx - https://www.archlinux.org/packages/community/any/letshelp-letsencrypt -From `master`: https://aur.archlinux.org/packages/letsencrypt-git +From ``master``: https://aur.archlinux.org/packages/letsencrypt-git ### Debian (and its derivatives, including Ubuntu) From 505b8854f67b95e84217a7833c02a92ee79191d2 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 9 May 2016 14:46:37 -0700 Subject: [PATCH 051/445] break sections correctly --- docs/packaging.rst | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/docs/packaging.rst b/docs/packaging.rst index 8ec7d7fca..0113fbbe6 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -19,6 +19,7 @@ The following scripts are used in the process: - https://gist.github.com/kuba/b9a3a2ca3bd35b8368ef We currently version as ``0.0.0.devYYYYMMDD``, and will change at GA time to the following scheme: + - ``0.1.0`` - ``0.2.0dev`` for developement in ``master`` - ``0.2.0`` (only temporarily in ``master``) @@ -26,7 +27,8 @@ We currently version as ``0.0.0.devYYYYMMDD``, and will change at GA time to the Tracking issue for non-dev release scripts: https://github.com/letsencrypt/letsencrypt/issues/1185 -## Notes for package maintainers +Notes for package maintainers +============================= 0. Please use our releases, not ``master``! @@ -40,11 +42,12 @@ Tracking issue for non-dev release scripts: https://github.com/letsencrypt/letse 5. Do get in touch with us. We are happy to make any changes that will make packaging easier. If you need to apply some patches don't do it downstream - make a PR here. -## Already ongoing efforts +Already ongoing efforts +----------------------- - -### Arch +Arch +==== From PyPI: - https://www.archlinux.org/packages/community/any/python2-acme @@ -55,25 +58,32 @@ From PyPI: From ``master``: https://aur.archlinux.org/packages/letsencrypt-git -### Debian (and its derivatives, including Ubuntu) +Debian (and its derivatives, including Ubuntu) +====== https://alioth.debian.org/projects/letsencrypt/ -### Fedora +Fedora +====== + In Fedora 23+. - https://admin.fedoraproject.org/pkgdb/package/letsencrypt/ - https://admin.fedoraproject.org/pkgdb/package/python-acme/ - https://bugzilla.redhat.com/show_bug.cgi?id=1287193 (review request, closed) -### FreeBSD +FreeBSD +======= https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=203405 -### GNU Guix +GNU Guix +======== + - https://www.gnu.org/software/guix/package-list.html#letsencrypt -### OpenBSD +OpenBSD +======= - http://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/security/letsencrypt/ - https://github.com/letsencrypt/letsencrypt/pull/1175 From c4c1fa3a26b8291532d936f194b89a4ffea45081 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 9 May 2016 14:48:28 -0700 Subject: [PATCH 052/445] switch size of subheadings --- docs/packaging.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/packaging.rst b/docs/packaging.rst index 0113fbbe6..6bf359bf2 100644 --- a/docs/packaging.rst +++ b/docs/packaging.rst @@ -43,11 +43,11 @@ Notes for package maintainers 5. Do get in touch with us. We are happy to make any changes that will make packaging easier. If you need to apply some patches don't do it downstream - make a PR here. Already ongoing efforts ------------------------ +======================= Arch -==== +---- From PyPI: - https://www.archlinux.org/packages/community/any/python2-acme @@ -59,12 +59,12 @@ From PyPI: From ``master``: https://aur.archlinux.org/packages/letsencrypt-git Debian (and its derivatives, including Ubuntu) -====== +------ https://alioth.debian.org/projects/letsencrypt/ Fedora -====== +------ In Fedora 23+. @@ -73,17 +73,17 @@ In Fedora 23+. - https://bugzilla.redhat.com/show_bug.cgi?id=1287193 (review request, closed) FreeBSD -======= +------- https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=203405 GNU Guix -======== +-------- - https://www.gnu.org/software/guix/package-list.html#letsencrypt OpenBSD -======= +------- - http://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/security/letsencrypt/ - https://github.com/letsencrypt/letsencrypt/pull/1175 From 63c79f98ca44453c3fe68c37ddcacb5b7bed938c Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 18 May 2016 10:59:58 +0300 Subject: [PATCH 053/445] Remove dangling footnote This footnote has no references! --- docs/using.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 12ea3ea11..e81aca16a 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -552,10 +552,3 @@ Beyond the methods discussed here, other methods may be possible, such as installing Certbot directly with pip from PyPI or downloading a ZIP archive from GitHub may be technically possible but are not presently recommended or supported. - - -.. rubric:: Footnotes - -.. [#venv] By using this virtualized Python environment (`virtualenv - `_) we don't pollute the main - OS space with packages from PyPI! From 12a356298c6217e5b9934262742ebed04c1ddfec Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 20:34:38 +0200 Subject: [PATCH 054/445] Displaying DialogError details correctly --- certbot/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/certbot/main.py b/certbot/main.py index fa5d43b72..b5407370a 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -623,7 +623,11 @@ def _handle_exception(exc_type, exc_value, trace, config): # Here we're passing a client or ACME error out to the client at the shell # Tell the user a bit about what happened, without overwhelming # them with a full traceback - err = traceback.format_exception_only(exc_type, exc_value)[0] + from dialog import DialogError + if issubclass(exc_type, DialogError): + err = exc_value.complete_message() + else: + err = traceback.format_exception_only(exc_type, exc_value)[0] # Typical error from the ACME module: # acme.messages.Error: urn:acme:error:malformed :: The request message was # malformed :: Error creating new registration :: Validation of contact From 4a8f71277c51d8cb26970f98bdbbc8ee67396397 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 20 May 2016 21:25:44 +0200 Subject: [PATCH 055/445] Limiting tox envlist to really needed tests --- docs/contributing.rst | 3 +++ tox.ini | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 3318ec103..9ceb0fbdd 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -71,6 +71,9 @@ The following tools are there to help you: experimental, non-production Apache2 install on them. ``tox -e apacheconftest`` can be used to run those specific Apache conf tests. +- ``tox --skip-missing-interpreters`` runs tox while ignoring missing versions + of Python needed for running the tests. + - ``tox -e py27``, ``tox -e py26`` etc, run unit tests for specific Python versions. diff --git a/tox.ini b/tox.ini index 5c88dfd21..4067bada9 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ [tox] skipsdist = true -envlist = py{26,27,33,34,35},py{26,27}-oldest,cover,lint +envlist = py{26,33,34,35},cover,lint # nosetest -v => more verbose output, allows to detect busy waiting # loops, especially on Travis From 928ea2f5cb7edf22d77ff3bc4bbca1b36c92b7cd Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Sat, 28 May 2016 02:18:49 +0200 Subject: [PATCH 056/445] Fixing minute problems with code --- certbot/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 235d7c3ff..9c8849cfe 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -1,6 +1,7 @@ """Certbot main entry point.""" from __future__ import print_function import atexit +import dialog import functools import logging.handlers import os @@ -623,8 +624,7 @@ def _handle_exception(exc_type, exc_value, trace, config): # Here we're passing a client or ACME error out to the client at the shell # Tell the user a bit about what happened, without overwhelming # them with a full traceback - from dialog import DialogError - if issubclass(exc_type, DialogError): + if issubclass(exc_type, dialog.error): err = exc_value.complete_message() else: err = traceback.format_exception_only(exc_type, exc_value)[0] From d21f8fa657cf4fbea47da47f1dc0bb5e1c9e594a Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Fri, 20 May 2016 08:50:00 +0300 Subject: [PATCH 057/445] Add --disable-hook-validation As discussed in #3020. --- certbot/cli.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index bcb7785c5..f3decec05 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -356,7 +356,8 @@ class HelpfulArgumentParser(object): " {0} conflicts with dialog_mode").format(arg) ) - hooks.validate_hooks(parsed_args) + if parsed_args.validate_hooks: + hooks.validate_hooks(parsed_args) return parsed_args @@ -792,6 +793,14 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "For this command, the shell variable $RENEWED_LINEAGE will point to the" "config live subdirectory containing the new certs and keys; the shell variable " "$RENEWED_DOMAINS will contain a space-delimited list of renewed cert domains") + helpful.add( + "renew", "--disable-hook-validation", + action='store_false', dest='validate_hooks', default=True, + help="Ordinarily the commands specified for --pre-hook/--post-hook/--renew-hook" + " will be checked for validity, to see if the programs being run are in the $PATH," + " so that mistakes can be caught early, even when the hooks aren't being run just yet." + " The validation is rather simplistic and fails if you use more advanced" + " shell constructs, so you can use this switch to disable it.") helpful.add_deprecated_argument("--agree-dev-preview", 0) From dcadcf8d42c6911a1c35fcd08590f693a68c8fef Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 13:50:30 -0700 Subject: [PATCH 058/445] Release 0.8.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 29 ++++++++---------- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 6 ++++ letsencrypt-auto | 29 ++++++++---------- letsencrypt-auto-source/certbot-auto.asc | 14 ++++----- letsencrypt-auto-source/letsencrypt-auto | 20 ++++++------ letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 18 +++++------ 12 files changed, 63 insertions(+), 63 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index c25cb5c00..cf3aa4df1 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0.dev0' +version = '0.8.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 2a4716db7..d85b33f3c 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0.dev0' +version = '0.8.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 5fbef43b1..2de5ff48f 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.7.0" +LE_AUTO_VERSION="0.8.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -713,24 +713,21 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc - -# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. - -acme==0.7.0 \ - --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ - --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 -certbot==0.7.0 \ - --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ - --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 -certbot-apache==0.7.0 \ - --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ - --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -letsencrypt-apache==0.7.0 \ - --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ - --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 + +# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. + +acme==0.8.0 \ + --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ + --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da +certbot==0.8.0 \ + --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ + --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 +certbot-apache==0.8.0 \ + --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ + --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 8d2bd925d..15a2af6e2 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0.dev0' +version = '0.8.0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index bb8b3414e..a710739f9 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0.dev0' +version = '0.8.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index dc0e2764d..98971584a 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.8.0.dev0' +__version__ = '0.8.0' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index 4026f1cc8..b25326148 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -10,6 +10,7 @@ cert. Major SUBCOMMANDS are: install Install a previously obtained cert in a server renew Renew previously obtained certs that are near expiry revoke Revoke a previously obtained certificate + register Perform tasks related to registering with the CA rollback Rollback server configuration changes made during install config_changes Show changes made to server config during installation plugins Display information about installed plugins @@ -53,6 +54,11 @@ optional arguments: to the Subscriber Agreement will still affect you, and will be effective 14 days after posting an update to the web site. (default: False) + --update-registration + With the register verb, indicates that details + associated with an existing registration, such as the + e-mail address, should be updated, rather than + registering a new account. (default: False) -m EMAIL, --email EMAIL Email used for registration and recovery contact. (default: None) diff --git a/letsencrypt-auto b/letsencrypt-auto index 5fbef43b1..2de5ff48f 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.7.0" +LE_AUTO_VERSION="0.8.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -713,24 +713,21 @@ zope.interface==4.1.3 \ mock==1.0.1 \ --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc - -# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. - -acme==0.7.0 \ - --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ - --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 -certbot==0.7.0 \ - --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ - --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 -certbot-apache==0.7.0 \ - --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ - --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -letsencrypt-apache==0.7.0 \ - --hash=sha256:10445980a6afc810325ea22a56e269229999120848f6c0b323b00275696b5c80 \ - --hash=sha256:3f4656088a18e4efea7cd7eb4965e14e8d901f3b64f4691e79cafd0bb91890f0 + +# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. + +acme==0.8.0 \ + --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ + --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da +certbot==0.8.0 \ + --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ + --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 +certbot-apache==0.8.0 \ + --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ + --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 454cbe598..0255229b0 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJXSK5DAAoJEE0XyZXNl3Xyyb4H/Ahy9/8ADDaN5V/O/6kl6gE5 -amQfm8T10EUD8APnNWYrYKBYruDBVvH0KiEcuAEs7q4xE5BaQatlobSnsHfv4AWW -TwInk2lRxYZ++MwwQf3DrqMK5QKfcoVnViZsRpZ8gHMLzsJllRm7R5eaTewO2ViM -KM+yDB3UsquLUvE4d3/hgBl2mXAUwsxLeFreZayvpoTcX2ARnzbtKqMaIBYDYWcx -DewWtDsPrhKFpb2DY06S6JLmEttysUgv+hbKlaVO0yZ8cCUehkzBIGYoeS4chOLq -fonNCzB8u3RtnLEFiPIy0N+A592jbLsqqUkxjammaJq3lH7nitduMLnpvGKt4yc= -=ex1J +iQEcBAABAgAGBQJXUJvwAAoJEE0XyZXNl3XyvKsH/3qn7Xa/GQx3HvB6Io/Csn/E +v1nbUg5RPwvrTyyol8BJ6UrHiJw+gTbUgCAnBkZ7DYKaC8AQmQXVRcWXNALMMTzB +6LpBXjQQ2xrBYamGj70N7KnTM1QmxI96GUQouiHMJVugV4uihKJDjtR8/f2JWKok +ZSox6E4LqC45HzqLWiOqc13TrHbti32Mo8DyC63PBnSwMnypGLK6XcqM0L9Re62W +smoKu1VWKwWZYRYXIQr0dvK4JmVTrIsdASdZkhTC/vc8y4tGkdN0DcF2EHzci6OA +Tx0W+Ao+HM1ZcaaH3BJ1y3kYfT+mlt6o4OaK3UB/wtUzMmVih7l1UeiNkVL0oYk= +=t3L6 -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..2de5ff48f 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.0.dev0" +LE_AUTO_VERSION="0.8.0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -719,15 +719,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.7.0 \ - --hash=sha256:6e61dba343806ad4cb27af84628152abc9e83a0fa24be6065587d2b46f340d7a \ - --hash=sha256:9f75a1947978402026b741bdee8a18fc5a1cfd539b78e523b7e5f279bf18eeb9 -certbot==0.7.0 \ - --hash=sha256:55604e43d231ac226edefed8dc110d792052095c3d75ad0e4a228ae0989fe5fd \ - --hash=sha256:ad5083d75e16d1ab806802d3a32f34973b6d7adaf083aee87e07a6c1359efe88 -certbot-apache==0.7.0 \ - --hash=sha256:5ab5ed9b2af6c7db9495ce1491122798e9d0764e3df8f0843d11d89690bf7f88 \ - --hash=sha256:1ddbfaf01bcb0b05c0dcc8b2ebd37637f080cf798151e8140c20c9f5fe7bae75 +acme==0.8.0 \ + --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ + --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da +certbot==0.8.0 \ + --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ + --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 +certbot-apache==0.8.0 \ + --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ + --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index e7e6546a25178083f007351bbba85fda56c87bb2..f024577a34a42779e39755635eca4daf6f46ac2a 100644 GIT binary patch literal 256 zcmV+b0ssE80bqRH{`qI)8kxNPxBYvrI}8PM??lS5W%Vrx#G#77ah$EmwK78hzI8fL zpFoy+Z$Ff?%2R#7_m4mldi$VK+OCedjDk7*K$j_4-%~DH(I7GKai+aBwofiSx4Q27 z;SV#~5cO^_zi`sHUA566Xqu@Q&d>p~uV#$A5kiL^Wt5QWl@grCRt$tN>*5-L&1i=@?vr?8a-~$mStKTr+dwJTPqN4;)GA0wY_jCnH7Wesi1I|l7M@y GRi#to9)G0( literal 256 zcmV+b0ssD(3r1$c09~FFRL*(rn=D5xr$W>4tf{yJ0Yd>ifFOs3`#iS_w(uP+p(fG& zTEmPXK+5%ZpiHyaPtmN4i1xvJn%fgm9b2nF4!4kuT8ov;Uq`~S8bQzaVDw Date: Thu, 2 Jun 2016 13:50:37 -0700 Subject: [PATCH 059/445] Bump version to 0.9.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-auto-source/letsencrypt-auto | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index cf3aa4df1..ed133e128 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index d85b33f3c..e3dbe4563 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 15a2af6e2..fe2c0c9d0 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0' +version = '0.9.0.dev0' install_requires = [ 'certbot=={0}'.format(version), diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index a710739f9..62c705b4c 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.0' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 98971584a..34358a5d9 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.8.0' +__version__ = '0.9.0.dev0' diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 2de5ff48f..89d345062 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.0" +LE_AUTO_VERSION="0.9.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates From 76a939ceb3c37419e4e9eccde29c558ae332ad56 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 16:00:19 -0700 Subject: [PATCH 060/445] Exit if cannot bootstrap --- letsencrypt-auto-source/letsencrypt-auto.template | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 43d8bc7e1..07f52147b 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -197,6 +197,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } From c9bdc19851f2bae586c27663747723535fe19a51 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 2 Jun 2016 16:03:15 -0700 Subject: [PATCH 061/445] Build letsencrypt-auto --- letsencrypt-auto-source/letsencrypt-auto | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..a5d699995 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -500,6 +500,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } From 6a0c6c85fb7028fdc3793325d40681b7c34f777f Mon Sep 17 00:00:00 2001 From: bmw Date: Thu, 2 Jun 2016 16:42:55 -0700 Subject: [PATCH 062/445] Revert "Use --force-reinstall to fix bad virtualenv package" --- tools/_venv_common.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/_venv_common.sh b/tools/_venv_common.sh index dc6ca3dd2..a121af82d 100755 --- a/tools/_venv_common.sh +++ b/tools/_venv_common.sh @@ -18,8 +18,7 @@ virtualenv --no-site-packages $VENV_NAME $VENV_ARGS # Separately install setuptools and pip to make sure following # invocations use latest pip install -U setuptools -# --force-reinstall used to fix broken pip installation on some systems -pip install --force-reinstall -U pip +pip install -U pip pip install "$@" set +x From 6b7a76442e282e7d926c27cc405dcf10a999652f Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Thu, 2 Jun 2016 23:04:14 -0500 Subject: [PATCH 063/445] Update letsencrypt-auto for Arch's new package name --- letsencrypt-auto-source/letsencrypt-auto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..871c64d0c 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -476,7 +476,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." From f5c8a63c1896919056f8a7b7dd53eae0252afacb Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Fri, 3 Jun 2016 09:44:12 +0300 Subject: [PATCH 064/445] Tests for --disable-hook-validation --- certbot/tests/cli_test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 671da16f0..adbde1d3e 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -651,6 +651,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods out = stdout.getvalue() self.assertEqual("", out) + def test_renew_hook_validation(self): + self._make_test_renewal_conf('sample-renewal.conf') + args = ["renew", "--dry-run", "--post-hook=no-such-command"] + self._test_renewal_common(True, [], args=args, should_renew=False, + error_expected=True) + + def test_renew_no_hook_validation(self): + self._make_test_renewal_conf('sample-renewal.conf') + args = ["renew", "--dry-run", "--post-hook=no-such-command", + "--disable-hook-validation"] + self._test_renewal_common(True, [], args=args, should_renew=True, + error_expected=False) @mock.patch("certbot.cli.set_by_cli") def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): From 2815361e6381a12dd50e5c0fa88bbd6ec3b8775e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 3 Jun 2016 11:12:49 -0700 Subject: [PATCH 065/445] Update the template as well --- letsencrypt-auto-source/letsencrypt-auto.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 43d8bc7e1..73d819b4a 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -173,7 +173,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." From 81cda2903a11b660a88b4b346dd4d9870b9e8260 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 3 Jun 2016 15:30:20 -0700 Subject: [PATCH 066/445] Attempt at putting everything inside Docker --- .../configurators/apache/a2dismod.sh | 14 -- .../configurators/apache/a2enmod.sh | 18 --- .../configurators/apache/apache24.py | 63 -------- .../configurators/apache/common.py | 150 +----------------- .../configurators/common.py | 83 +--------- .../certbot_compatibility_test/test_driver.py | 4 +- .../certbot_compatibility_test/util.py | 10 -- 7 files changed, 5 insertions(+), 337 deletions(-) delete mode 100755 certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh delete mode 100755 certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh delete mode 100644 certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh deleted file mode 100755 index ca96e216f..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2dismod.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -# An extremely simplified version of `a2enmod` for disabling modules in the -# httpd docker image. First argument is the server_root and the second is the -# module to be disabled. - -apache_confdir=$1 -module=$2 - -sed -i "/.*"$module".*/d" "$apache_confdir/test.conf" -enabled_conf="$apache_confdir/mods-enabled/"$module".conf" -if [ -e "$enabled_conf" ] -then - rm $enabled_conf -fi diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh deleted file mode 100755 index 4da9288a2..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/a2enmod.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# An extremely simplified version of `a2enmod` for enabling modules in the -# httpd docker image. First argument is the Apache ServerRoot which should be -# an absolute path. The second is the module to be enabled, such as `ssl`. - -confdir=$1 -module=$2 - -echo "LoadModule ${module}_module " \ - "/usr/local/apache2/modules/mod_${module}.so" >> "${confdir}/test.conf" -availbase="/mods-available/${module}.conf" -availconf=$confdir$availbase -enabldir="$confdir/mods-enabled" -enablconf="$enabldir/${module}.conf" -if [ -e $availconf -a -d $enabldir -a ! -e $enablconf ] -then - ln -s "..$availbase" $enablconf -fi diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py deleted file mode 100644 index 927c329ef..000000000 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/apache24.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Proxies ApacheConfigurator for Apache 2.4 tests""" - -import zope.interface - -from certbot_compatibility_test import errors -from certbot_compatibility_test import interfaces -from certbot_compatibility_test.configurators.apache import common as apache_common - - -# The docker image doesn't actually have the watchdog module, but unless the -# config uses mod_heartbeat or mod_heartmonitor (which aren't installed and -# therefore the config won't be loaded), I believe this isn't a problem -# http://httpd.apache.org/docs/2.4/mod/mod_watchdog.html -STATIC_MODULES = set(["core", "so", "http", "mpm_event", "watchdog"]) - - -SHARED_MODULES = { - "log_config", "logio", "version", "unixd", "access_compat", "actions", - "alias", "allowmethods", "auth_basic", "auth_digest", "auth_form", - "authn_anon", "authn_core", "authn_dbd", "authn_dbm", "authn_file", - "authn_socache", "authnz_ldap", "authz_core", "authz_dbd", "authz_dbm", - "authz_groupfile", "authz_host", "authz_owner", "authz_user", "autoindex", - "buffer", "cache", "cache_disk", "cache_socache", "cgid", "dav", "dav_fs", - "dbd", "deflate", "dir", "dumpio", "env", "expires", "ext_filter", - "file_cache", "filter", "headers", "include", "info", "lbmethod_bybusyness", - "lbmethod_byrequests", "lbmethod_bytraffic", "lbmethod_heartbeat", "ldap", - "log_debug", "macro", "mime", "negotiation", "proxy", "proxy_ajp", - "proxy_balancer", "proxy_connect", "proxy_express", "proxy_fcgi", - "proxy_ftp", "proxy_http", "proxy_scgi", "proxy_wstunnel", "ratelimit", - "remoteip", "reqtimeout", "request", "rewrite", "sed", "session", - "session_cookie", "session_crypto", "session_dbd", "setenvif", - "slotmem_shm", "socache_dbm", "socache_memcache", "socache_shmcb", - "speling", "ssl", "status", "substitute", "unique_id", "userdir", - "vhost_alias"} - - -@zope.interface.implementer(interfaces.IConfiguratorProxy) -class Proxy(apache_common.Proxy): - """Wraps the ApacheConfigurator for Apache 2.4 tests""" - - def __init__(self, args): - """Initializes the plugin with the given command line args""" - super(Proxy, self).__init__(args) - # Running init isn't ideal, but the Docker container needs to survive - # Apache restarts - self.start_docker("bradmw/apache2.4", "init") - - def preprocess_config(self, server_root): - """Prepares the configuration for use in the Docker""" - super(Proxy, self).preprocess_config(server_root) - if self.version[1] != 4: - raise errors.Error("Apache version not 2.4") - - with open(self.test_conf, "a") as f: - for module in self.modules: - if module not in STATIC_MODULES: - if module in SHARED_MODULES: - f.write( - "LoadModule {0}_module /usr/local/apache2/modules/" - "mod_{0}.so\n".format(module)) - else: - raise errors.Error( - "Unsupported module {0}".format(module)) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 9148666fc..7af0ee20e 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -29,58 +29,9 @@ class Proxy(configurators_common.Proxy): super(Proxy, self).__init__(args) self.le_config.apache_le_vhost_ext = "-le-ssl.conf" - self._setup_mock() - self.modules = self.server_root = self.test_conf = self.version = None self._apache_configurator = self._all_names = self._test_names = None - def _setup_mock(self): - """Replaces specific modules with mock.MagicMock""" - mock_subprocess = mock.MagicMock() - mock_subprocess.check_call = self.check_call - mock_subprocess.Popen = self.popen - - mock.patch( - "certbot_apache.configurator.subprocess", - mock_subprocess).start() - mock.patch( - "certbot_apache.parser.subprocess", - mock_subprocess).start() - mock.patch( - "certbot.util.subprocess", - mock_subprocess).start() - mock.patch( - "certbot_apache.configurator.util.exe_exists", - _is_apache_command).start() - - patch = mock.patch( - "certbot_apache.configurator.display_ops.select_vhost") - mock_display = patch.start() - mock_display.side_effect = le_errors.PluginError( - "Unable to determine vhost") - - def check_call(self, command, *args, **kwargs): - """If command is an Apache command, command is executed in the - running docker image. Otherwise, subprocess.check_call is used. - - """ - if _is_apache_command(command): - command = _modify_command(command) - return super(Proxy, self).check_call(command, *args, **kwargs) - else: - return subprocess.check_call(command, *args, **kwargs) - - def popen(self, command, *args, **kwargs): - """If command is an Apache command, command is executed in the - running docker image. Otherwise, subprocess.Popen is used. - - """ - if _is_apache_command(command): - command = _modify_command(command) - return super(Proxy, self).popen(command, *args, **kwargs) - else: - return subprocess.Popen(command, *args, **kwargs) - def __getattr__(self, name): """Wraps the Apache Configurator methods""" method = getattr(self._apache_configurator, name, None) @@ -91,28 +42,19 @@ class Proxy(configurators_common.Proxy): def load_config(self): """Loads the next configuration for the plugin to test""" - if hasattr(self.le_config, "apache_init_script"): - try: - self.check_call([self.le_config.apache_init_script, "stop"]) - except errors.Error: - raise errors.Error( - "Failed to stop previous apache config from running") config = super(Proxy, self).load_config() - self.modules = _get_modules(config) - self.version = _get_version(config) self._all_names, self._test_names = _get_names(config) server_root = _get_server_root(config) with open(os.path.join(config, "config_file")) as f: config_file = os.path.join(server_root, f.readline().rstrip()) - self.test_conf = _create_test_conf(server_root, config_file) self.preprocess_config(server_root) self._prepare_configurator(server_root, config_file) try: - self.check_call("apachectl -d {0} -f {1} -k start".format( + subprocess.check_call("apachectl -d {0} -f {1} -k start".format( server_root, config_file)) except errors.Error: raise errors.Error( @@ -121,34 +63,9 @@ class Proxy(configurators_common.Proxy): return config - def preprocess_config(self, server_root): - # pylint: disable=anomalous-backslash-in-string, no-self-use - """Prepares the configuration for use in the Docker""" - - find = subprocess.Popen( - ["find", server_root, "-type", "f"], - stdout=subprocess.PIPE) - subprocess.check_call([ - "xargs", "sed", "-e", "s/DocumentRoot.*/DocumentRoot " - "\/usr\/local\/apache2\/htdocs/I", - "-e", "s/SSLPassPhraseDialog.*/SSLPassPhraseDialog builtin/I", - "-e", "s/TypesConfig.*/TypesConfig " - "\/usr\/local\/apache2\/conf\/mime.types/I", - "-e", "s/LoadModule/#LoadModule/I", - "-e", "s/SSLCertificateFile.*/SSLCertificateFile " - "\/usr\/local\/apache2\/conf\/empty_cert.pem/I", - "-e", "s/SSLCertificateKeyFile.*/SSLCertificateKeyFile " - "\/usr\/local\/apache2\/conf\/rsa1024_key2.pem/I", - "-i"], stdin=find.stdout) - def _prepare_configurator(self, server_root, config_file): """Prepares the Apache plugin for testing""" self.le_config.apache_server_root = server_root - self.le_config.apache_ctl = "apachectl -d {0} -f {1}".format( - server_root, config_file) - self.le_config.apache_enmod = "a2enmod.sh {0}".format(server_root) - self.le_config.apache_dismod = "a2dismod.sh {0}".format(server_root) - self.le_config.apache_init_script = self.le_config.apache_ctl + " -k" self._apache_configurator = configurator.ApacheConfigurator( config=configuration.NamespaceConfig(self.le_config), @@ -158,7 +75,6 @@ class Proxy(configurators_common.Proxy): def cleanup_from_tests(self): """Performs any necessary cleanup from running plugin tests""" super(Proxy, self).cleanup_from_tests() - mock.patch.stopall() def get_all_names_answer(self): """Returns the set of domain names that the plugin should find""" @@ -183,39 +99,6 @@ class Proxy(configurators_common.Proxy): domain, cert_path, key_path, chain_path, fullchain_path) -def _is_apache_command(command): - """Returns true if command is an Apache command""" - if isinstance(command, list): - command = command[0] - - for apache_command in APACHE_COMMANDS: - if command.startswith(apache_command): - return True - - return False - - -def _modify_command(command): - """Modifies command so configtest works inside the docker image""" - if isinstance(command, list): - for i in xrange(len(command)): - if command[i] == "configtest": - command[i] = "-t" - else: - command = command.replace("configtest", "-t") - - return command - - -def _create_test_conf(server_root, apache_config): - """Creates a test config file and adds it to the Apache config""" - test_conf = os.path.join(server_root, "test.conf") - open(test_conf, "w").close() - subprocess.check_call( - ["sed", "-i", "1iInclude test.conf", apache_config]) - return test_conf - - def _get_server_root(config): """Returns the server root directory in config""" subdirs = [ @@ -251,34 +134,3 @@ def _get_names(config): words[1].find(".") != -1): all_names.add(words[1]) return all_names, non_ip_names - - -def _get_modules(config): - """Returns the list of modules found in module_list""" - modules = [] - with open(os.path.join(config, "modules")) as f: - for line in f: - # Modules list is indented, everything else is headers/footers - if line[0].isspace(): - words = line.split() - # Modules redundantly end in "_module" which we can discard - modules.append(words[0][:-7]) - - return modules - - -def _get_version(config): - """Return version of Apache Server. - - Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7)). Code taken from - the Apache plugin. - - """ - with open(os.path.join(config, "version")) as f: - # Should be on first line of input - matches = APACHE_VERSION_REGEX.findall(f.readline()) - - if len(matches) != 1: - raise errors.Error("Unable to find Apache version") - - return tuple([int(i) for i in matches[0].split(".")]) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py index 4657883a3..4592eca39 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py @@ -4,8 +4,6 @@ import os import shutil import tempfile -import docker - from certbot import constants from certbot_compatibility_test import errors from certbot_compatibility_test import util @@ -18,20 +16,9 @@ class Proxy(object): # pylint: disable=too-many-instance-attributes """A common base for compatibility test configurators""" - _NOT_ADDED_ARGS = True - @classmethod def add_parser_arguments(cls, parser): """Adds command line arguments needed by the plugin""" - if Proxy._NOT_ADDED_ARGS: - group = parser.add_argument_group("docker") - group.add_argument( - "--docker-url", default="unix://var/run/docker.sock", - help="URL of the docker server") - group.add_argument( - "--no-remove", action="store_true", - help="do not delete container on program exit") - Proxy._NOT_ADDED_ARGS = False def __init__(self, args): """Initializes the plugin with the given command line args""" @@ -43,10 +30,8 @@ class Proxy(object): for config in os.listdir(config_dir)] self.args = args - self._docker_client = docker.Client( - base_url=self.args.docker_url, version="auto") - self.http_port, self.https_port = util.get_two_free_ports() - self._container_id = None + self.http_port = 80 + self.https_port = 443 def has_more_configs(self): """Returns true if there are more configs to test""" @@ -54,9 +39,6 @@ class Proxy(object): def cleanup_from_tests(self): """Performs any necessary cleanup from running plugin tests""" - self._docker_client.stop(self._container_id, 0) - if not self.args.no_remove: - self._docker_client.remove_container(self._container_id) def load_config(self): """Returns the next config directory to be tested""" @@ -65,67 +47,6 @@ class Proxy(object): os.makedirs(backup) return self._configs.pop() - def start_docker(self, image_name, command): - """Creates and runs a Docker container with the specified image""" - logger.warning("Pulling Docker image. This may take a minute.") - for line in self._docker_client.pull(image_name, stream=True): - logger.debug(line) - - host_config = docker.utils.create_host_config( - binds={self._temp_dir: {"bind": self._temp_dir, "mode": "rw"}}, - port_bindings={ - 80: ("127.0.0.1", self.http_port), - 443: ("127.0.0.1", self.https_port)},) - container = self._docker_client.create_container( - image_name, command, ports=[80, 443], volumes=self._temp_dir, - host_config=host_config) - if container["Warnings"]: - logger.warning(container["Warnings"]) - self._container_id = container["Id"] - self._docker_client.start(self._container_id) - - def check_call(self, command, *args, **kwargs): - # pylint: disable=unused-argument - """Simulates a call to check_call but executes the command in the - running docker image - - """ - if self.popen(command).returncode: - raise errors.Error( - "{0} exited with a nonzero value".format(command)) - - def popen(self, command, *args, **kwargs): - # pylint: disable=unused-argument - """Simulates a call to Popen but executes the command in the - running docker image - - """ - class SimplePopen(object): - # pylint: disable=too-few-public-methods - """Simplified Popen object""" - def __init__(self, returncode, output): - self.returncode = returncode - self._stdout = output - self._stderr = output - - def communicate(self): - """Returns stdout and stderr""" - return self._stdout, self._stderr - - if isinstance(command, list): - command = " ".join(command) - - returncode, output = self.execute_in_docker(command) - return SimplePopen(returncode, output) - - def execute_in_docker(self, command): - """Executes command inside the running docker image""" - logger.debug("Executing '%s'", command) - exec_id = self._docker_client.exec_create(self._container_id, command) - output = self._docker_client.exec_start(exec_id) - returncode = self._docker_client.exec_inspect(exec_id)["ExitCode"] - return returncode, output - def copy_certs_and_keys(self, cert_path, key_path, chain_path=None): """Copies certs and keys into the temporary directory""" cert_and_key_dir = os.path.join(self._temp_dir, "certs_and_keys") diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 6823dfdab..ad5755d60 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -25,8 +25,8 @@ from certbot_compatibility_test.configurators.apache import apache24 DESCRIPTION = """ -Tests Certbot plugins against different server configuratons. It is -assumed that Docker is already installed. If no test types is specified, all +Tests Certbot plugins against different server configurations. It is +assumed that Docker is already installed. If no test type is specified, all tests that the plugin supports are performed. """ diff --git a/certbot-compatibility-test/certbot_compatibility_test/util.py b/certbot-compatibility-test/certbot_compatibility_test/util.py index cbce4fb56..570bf1a9e 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/util.py +++ b/certbot-compatibility-test/certbot_compatibility_test/util.py @@ -52,13 +52,3 @@ def extract_configs(configs, parent_dir): raise errors.Error("Unknown configurations file type") return config_dir - - -def get_two_free_ports(): - """Returns two free ports to use for the tests""" - with contextlib.closing(socket.socket()) as sock1: - with contextlib.closing(socket.socket()) as sock2: - sock1.bind(("", 0)) - sock2.bind(("", 0)) - - return sock1.getsockname()[1], sock2.getsockname()[1] From c79924b7712e2bd770b4c1afec2b2ebb21e32a07 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 3 Jun 2016 16:35:10 -0700 Subject: [PATCH 067/445] Work in progress on removing_proxy --- .../configurators/apache/common.py | 15 +++++++++++++-- .../certbot_compatibility_test/test_driver.py | 7 ++++--- certbot-compatibility-test/setup.py | 5 ++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 7af0ee20e..696ef976a 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -9,6 +9,7 @@ import zope.interface from certbot import configuration from certbot import errors as le_errors from certbot_apache import configurator +from certbot_apache import constants from certbot_compatibility_test import errors from certbot_compatibility_test import interfaces from certbot_compatibility_test import util @@ -31,6 +32,11 @@ class Proxy(configurators_common.Proxy): self.modules = self.server_root = self.test_conf = self.version = None self._apache_configurator = self._all_names = self._test_names = None + patch = mock.patch( + "certbot_apache.configurator.display_ops.select_vhost") + mock_display = patch.start() + mock_display.side_effect = le_errors.PluginError( + "Unable to determine vhost") def __getattr__(self, name): """Wraps the Apache Configurator methods""" @@ -50,12 +56,11 @@ class Proxy(configurators_common.Proxy): with open(os.path.join(config, "config_file")) as f: config_file = os.path.join(server_root, f.readline().rstrip()) - self.preprocess_config(server_root) self._prepare_configurator(server_root, config_file) try: subprocess.check_call("apachectl -d {0} -f {1} -k start".format( - server_root, config_file)) + server_root, config_file).split()) except errors.Error: raise errors.Error( "Apache failed to load {0} before tests started".format( @@ -65,8 +70,13 @@ class Proxy(configurators_common.Proxy): def _prepare_configurator(self, server_root, config_file): """Prepares the Apache plugin for testing""" + for k in constants.CLI_DEFAULTS_DEBIAN.keys(): + setattr(self.le_config, "apache_" + k, constants.os_constant(k)) self.le_config.apache_server_root = server_root + # An alias + self.le_config.apache_handle_modules = self.le_config.apache_handle_mods + self._apache_configurator = configurator.ApacheConfigurator( config=configuration.NamespaceConfig(self.le_config), name="apache") @@ -75,6 +85,7 @@ class Proxy(configurators_common.Proxy): def cleanup_from_tests(self): """Performs any necessary cleanup from running plugin tests""" super(Proxy, self).cleanup_from_tests() + mock.patch.stopall() def get_all_names_answer(self): """Returns the set of domain names that the plugin should find""" diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index ad5755d60..ce9f590d5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -21,7 +21,7 @@ from certbot_compatibility_test import errors from certbot_compatibility_test import util from certbot_compatibility_test import validator -from certbot_compatibility_test.configurators.apache import apache24 +from certbot_compatibility_test.configurators.apache import common DESCRIPTION = """ @@ -31,7 +31,7 @@ tests that the plugin supports are performed. """ -PLUGINS = {"apache": apache24.Proxy} +PLUGINS = {"apache": common.Proxy} logger = logging.getLogger(__name__) @@ -47,6 +47,8 @@ def test_authenticator(plugin, config, temp_dir): "challenge types") return False + import ipdb + ipdb.set_trace() try: responses = plugin.perform(achalls) except le_errors.Error as error: @@ -341,7 +343,6 @@ def main(): temp_dir = tempfile.mkdtemp() plugin = PLUGINS[args.plugin](args) try: - plugin.execute_in_docker("mkdir -p /var/log/apache2") while plugin.has_more_configs(): success = True diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 8d2bd925d..07dfa1684 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -7,9 +7,8 @@ from setuptools import find_packages version = '0.8.0.dev0' install_requires = [ - 'certbot=={0}'.format(version), - 'certbot-apache=={0}'.format(version), - 'docker-py', + 'certbot', + 'certbot-apache', 'requests', 'zope.interface', ] From ec49afb7c05b5523b0eb8fc87f749478086d6a36 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 3 Jun 2016 16:45:24 -0700 Subject: [PATCH 068/445] UnspacedList: infrastructure for parsing but hiding nginx whitespace --- certbot-nginx/certbot_nginx/nginxparser.py | 68 +++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 1577c7b1e..f9348398a 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -1,4 +1,5 @@ """Very low-level nginx config parser based on pyparsing.""" +import copy import string from pyparsing import ( @@ -26,8 +27,8 @@ class RawNginxParser(object): modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") # rules - comment = Literal('#') + restOfLine() - assignment = (key + Optional(space + value, default=None) + semicolon) + comment = White() + Literal('#') + restOfLine() + assignment = White() + key + Optional(space + value, default=None) + semicolon location_statement = Optional(space + modifier) + Optional(space + location) if_statement = Literal("if") + space + Regex(r"\(.+\)") + space map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space @@ -136,3 +137,66 @@ def dump(blocks, _file, indentation=4): """ return _file.write(dumps(blocks, indentation)) + + + +spacey = lambda x: isinstance(x, str) and x.isspace() + +class UnspacedList(list): + """Wrap a list [of lists], making any whitespace entries magically invisible""" + + def __init__(self, list_source): + self.spaced = copy.deepcopy(list(list_source)) + + # Turn self into a version of the source list that has spaces removed + # and all sub-lists also UnspaceList()ed + list.__init__(self, list_source) + for i, entry in reversed(list(enumerate(self))): + if isinstance(entry, list): + list.__setitem__(self, i, UnspacedList(entry)) + elif spacey(entry): + list.__delitem__(self, i) + + def insert(self, i, x): + self.spaced.insert(i + self._spaces_before(i), x) + list.insert(self, i, x) + + def append(self, x): + self.spaced.append(x) + list.append(self, x) + + def extend(self, x): + self.spaced.extend(x) + list.extend(self, x) + + def __add__(self, other): + if hasattr(other, "spaced"): + # If the thing added to us is an UnspacedList, use its spaced form + self.spaced.__add__(other.spaced) + else: + self.spaced.__add__(other) + list.__add__(self, other) + + def __setitem__(self, i, value): + self.spaced.__setitem__(i + self._spaces_before(i), value) + list.__setitem__(self, i, value) + + def __delitem__(self, i): + self.spaced.__delitem__(i + self._spaces_before(i)) + list.__delitem__(self, i) + + def _spaces_before(self, idx): + "Count the number of spaces in the spaced list before pos idx in the spaceless one" + spaces = 0 + pos = 0 + while idx != -1: + if spacey(self.spaced[pos]): + spaces += 1 + else: + idx -= 1 + pos += 1 + return spaces + + def with_spaces(self): + return self.spaced + From 91cd19158e4f080a5f013b2c8af772f9d7004157 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Sat, 4 Jun 2016 22:53:51 -0700 Subject: [PATCH 069/445] Improve user experience for linting. Don't run pep8 for directories that we don't actually enforce pep8 on. Install dependencies with -q. Don't print reports, they make it hard to find the actual errors. Remove deprecated fields from acme .pylintrc, they cause unnecessary messages about deprecation. --- acme/.pylintrc | 6 ------ pep8.travis.sh | 12 ------------ tox.ini | 14 +++++++------- 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/acme/.pylintrc b/acme/.pylintrc index d0d150631..06bb2a01f 100644 --- a/acme/.pylintrc +++ b/acme/.pylintrc @@ -21,12 +21,6 @@ persistent=yes # usually to register additional checkers. load-plugins=linter_plugin -# DEPRECATED -include-ids=no - -# DEPRECATED -symbols=no - # Use multiple processes to speed up Pylint. jobs=1 diff --git a/pep8.travis.sh b/pep8.travis.sh index c13547a78..cadea8489 100755 --- a/pep8.travis.sh +++ b/pep8.travis.sh @@ -2,16 +2,4 @@ set -e # Fail fast -# PEP8 is not ignored in ACME pep8 --config=acme/.pep8 acme - -pep8 \ - setup.py \ - certbot \ - certbot-apache \ - certbot-nginx \ - certbot-compatibility-test \ - letshelp-certbot \ - || echo "PEP8 checking failed, but it's ignored in Travis" - -# echo exits with 0 diff --git a/tox.ini b/tox.ini index 5c88dfd21..cb625ba8d 100644 --- a/tox.ini +++ b/tox.ini @@ -64,14 +64,14 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot + pip install -q -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot ./pep8.travis.sh - pylint --rcfile=.pylintrc certbot - pylint --rcfile=acme/.pylintrc acme/acme - pylint --rcfile=.pylintrc certbot-apache/certbot_apache - pylint --rcfile=.pylintrc certbot-nginx/certbot_nginx - pylint --rcfile=.pylintrc certbot-compatibility-test/certbot_compatibility_test - pylint --rcfile=.pylintrc letshelp-certbot/letshelp_certbot + pylint --reports=n --rcfile=.pylintrc certbot + pylint --reports=n --rcfile=acme/.pylintrc acme/acme + pylint --reports=n --rcfile=.pylintrc certbot-apache/certbot_apache + pylint --reports=n --rcfile=.pylintrc certbot-nginx/certbot_nginx + pylint --reports=n --rcfile=.pylintrc certbot-compatibility-test/certbot_compatibility_test + pylint --reports=n --rcfile=.pylintrc letshelp-certbot/letshelp_certbot [testenv:apacheconftest] #basepython = python2.7 From 08ccc64cd1dc0c852ad4189bbc37aa451e7a611c Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 12:04:44 +0300 Subject: [PATCH 070/445] Initialize augeas in a new method --- certbot-apache/certbot_apache/augeas_configurator.py | 4 +++- certbot-apache/certbot_apache/configurator.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/augeas_configurator.py b/certbot-apache/certbot_apache/augeas_configurator.py index 12753541c..820a58438 100644 --- a/certbot-apache/certbot_apache/augeas_configurator.py +++ b/certbot-apache/certbot_apache/augeas_configurator.py @@ -1,7 +1,6 @@ """Class of Augeas Configurators.""" import logging -import augeas from certbot import errors from certbot import reverter @@ -29,6 +28,9 @@ class AugeasConfigurator(common.Plugin): def __init__(self, *args, **kwargs): super(AugeasConfigurator, self).__init__(*args, **kwargs) + def init_augeas(self): + """ Initialize the actual Augeas instance """ + import augeas self.aug = augeas.Augeas( # specify a directory to load our preferred lens from loadpath=constants.AUGEAS_LENS_DIR, diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index e4c06ba7e..9caa4a764 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -150,6 +150,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :raises .errors.PluginError: If there is any other error """ + # Perform the actual Augeas initialization to be able to react + try: + self.init_augeas() + except ImportError: + raise errors.NoInstallationError("Problem in Augeas installation") + # Verify Apache is installed if not util.exe_exists(constants.os_constant("restart_cmd")[0]): raise errors.NoInstallationError From 7239361342e0001cfc8ad958250564e50e8bd0cb Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 12:36:54 +0300 Subject: [PATCH 071/445] Test coverage for NoInstallationError --- certbot-apache/certbot_apache/tests/configurator_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index a2e39de47..57344bbb6 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -55,6 +55,14 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises( errors.NoInstallationError, self.config.prepare) + @mock.patch("certbot_apache.augeas_configurator.AugeasConfigurator.init_augeas") + def test_prepare_no_augeas(self, mock_init_augeas): + def side_effect_error(*args, **kwargs): + raise ImportError + mock_init_augeas.side_effect = side_effect_error + self.assertRaises( + errors.NoInstallationError, self.config.prepare) + @mock.patch("certbot_apache.parser.ApacheParser") @mock.patch("certbot_apache.configurator.util.exe_exists") def test_prepare_version(self, mock_exe_exists, _): From e2631322839e4a3b5117fb4581da72fa4c2eddc5 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 12:44:49 +0300 Subject: [PATCH 072/445] Refactor and lint fixes --- .../certbot_apache/augeas_configurator.py | 20 +++++++++++-------- .../certbot_apache/tests/configurator_test.py | 4 +++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/certbot-apache/certbot_apache/augeas_configurator.py b/certbot-apache/certbot_apache/augeas_configurator.py index 820a58438..478efb099 100644 --- a/certbot-apache/certbot_apache/augeas_configurator.py +++ b/certbot-apache/certbot_apache/augeas_configurator.py @@ -28,6 +28,18 @@ class AugeasConfigurator(common.Plugin): def __init__(self, *args, **kwargs): super(AugeasConfigurator, self).__init__(*args, **kwargs) + # Placeholder for augeas + self.aug = None + + self.save_notes = "" + + # See if any temporary changes need to be recovered + # This needs to occur before VirtualHost objects are setup... + # because this will change the underlying configuration and potential + # vhosts + self.reverter = reverter.Reverter(self.config) + self.recovery_routine() + def init_augeas(self): """ Initialize the actual Augeas instance """ import augeas @@ -37,14 +49,6 @@ class AugeasConfigurator(common.Plugin): # Do not save backup (we do it ourselves), do not load # anything by default flags=(augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)) - self.save_notes = "" - - # See if any temporary changes need to be recovered - # This needs to occur before VirtualHost objects are setup... - # because this will change the underlying configuration and potential - # vhosts - self.reverter = reverter.Reverter(self.config) - self.recovery_routine() def check_parsing_errors(self, lens): """Verify Augeas can parse all of the lens files. diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 57344bbb6..e5c09fd1d 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -57,7 +57,9 @@ class MultipleVhostsTest(util.ApacheTest): @mock.patch("certbot_apache.augeas_configurator.AugeasConfigurator.init_augeas") def test_prepare_no_augeas(self, mock_init_augeas): - def side_effect_error(*args, **kwargs): + """ Test augeas initialization ImportError """ + def side_effect_error(): + """ Side effect error for the test """ raise ImportError mock_init_augeas.side_effect = side_effect_error self.assertRaises( From 1f6e999153be1347d92eef884a297966c0e15801 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 6 Jun 2016 13:10:34 +0300 Subject: [PATCH 073/445] Move recovery_routine() to augeas init --- certbot-apache/certbot_apache/augeas_configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/augeas_configurator.py b/certbot-apache/certbot_apache/augeas_configurator.py index 478efb099..6999120d6 100644 --- a/certbot-apache/certbot_apache/augeas_configurator.py +++ b/certbot-apache/certbot_apache/augeas_configurator.py @@ -38,7 +38,6 @@ class AugeasConfigurator(common.Plugin): # because this will change the underlying configuration and potential # vhosts self.reverter = reverter.Reverter(self.config) - self.recovery_routine() def init_augeas(self): """ Initialize the actual Augeas instance """ @@ -49,6 +48,7 @@ class AugeasConfigurator(common.Plugin): # Do not save backup (we do it ourselves), do not load # anything by default flags=(augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)) + self.recovery_routine() def check_parsing_errors(self, lens): """Verify Augeas can parse all of the lens files. From 78ea886a79c3dc3b1c75a5d5eb3dca5abbd15219 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 11:49:36 -0700 Subject: [PATCH 074/445] Fix deploy cert and TLSSNI check --- .../certbot_compatibility_test/test_driver.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index ce9f590d5..885eacd92 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -47,8 +47,6 @@ def test_authenticator(plugin, config, temp_dir): "challenge types") return False - import ipdb - ipdb.set_trace() try: responses = plugin.perform(achalls) except le_errors.Error as error: @@ -63,7 +61,7 @@ def test_authenticator(plugin, config, temp_dir): "Plugin failed to complete %s for %s in %s", type(achalls[i]), achalls[i].domain, config) success = False - elif isinstance(responses[i], challenges.TLSSNI01): + elif isinstance(responses[i], challenges.TLSSNI01Response): verify = functools.partial(responses[i].simple_verify, achalls[i], achalls[i].domain, util.JWK.public_key(), @@ -144,7 +142,7 @@ def test_deploy_cert(plugin, temp_dir, domains): for domain in domains: try: - plugin.deploy_cert(domain, cert_path, util.KEY_PATH) + plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path) except le_errors.Error as error: logger.error("Plugin failed to deploy ceritificate for %s:", domain) logger.exception(error) From 1d3fbe945de27894578fdc79878b4ce5032edaa2 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 12:01:55 -0700 Subject: [PATCH 075/445] Copy config into /etc/apache2 --- .../configurators/apache/common.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 696ef976a..af27f7ed5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -53,14 +53,15 @@ class Proxy(configurators_common.Proxy): self._all_names, self._test_names = _get_names(config) server_root = _get_server_root(config) - with open(os.path.join(config, "config_file")) as f: - config_file = os.path.join(server_root, f.readline().rstrip()) + # with open(os.path.join(config, "config_file")) as f: + # config_file = os.path.join(server_root, f.readline().rstrip()) + shutil.rmtree("/etc/apache2") + shutil.copytree(server_root, "/etc/apache2", symlinks=True) - self._prepare_configurator(server_root, config_file) + self._prepare_configurator() try: - subprocess.check_call("apachectl -d {0} -f {1} -k start".format( - server_root, config_file).split()) + subprocess.check_call("apachectl -k start".split()) except errors.Error: raise errors.Error( "Apache failed to load {0} before tests started".format( @@ -68,11 +69,10 @@ class Proxy(configurators_common.Proxy): return config - def _prepare_configurator(self, server_root, config_file): + def _prepare_configurator(self): """Prepares the Apache plugin for testing""" for k in constants.CLI_DEFAULTS_DEBIAN.keys(): setattr(self.le_config, "apache_" + k, constants.os_constant(k)) - self.le_config.apache_server_root = server_root # An alias self.le_config.apache_handle_modules = self.le_config.apache_handle_mods From e0bb04fd2550701d5b553a1f1516fd17a096d611 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 12:02:53 -0700 Subject: [PATCH 076/445] Forgot to import shutil --- .../certbot_compatibility_test/configurators/apache/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index af27f7ed5..918db5f47 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -1,6 +1,7 @@ """Provides a common base for Apache proxies""" import re import os +import shutil import subprocess import mock From e1f4e22c6d5e158eb213745af571cba29dde3e86 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 12:09:17 -0700 Subject: [PATCH 077/445] Unwrap achall --- .../certbot_compatibility_test/test_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 885eacd92..165791684 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -62,7 +62,7 @@ def test_authenticator(plugin, config, temp_dir): type(achalls[i]), achalls[i].domain, config) success = False elif isinstance(responses[i], challenges.TLSSNI01Response): - verify = functools.partial(responses[i].simple_verify, achalls[i], + verify = functools.partial(responses[i].simple_verify, achalls[i].chall, achalls[i].domain, util.JWK.public_key(), host="127.0.0.1", From 111ca657d86e0caf31dbc81f83037a021fd22131 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Mon, 6 Jun 2016 21:17:02 +0200 Subject: [PATCH 078/445] Adding testing --- certbot/tests/cli_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 4ae69e69d..06aba4ab3 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -882,6 +882,15 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mock_sys.exit.assert_called_with(''.join( traceback.format_exception_only(KeyboardInterrupt, interrupt))) + # Test dialog errors + + import dialog + exception = dialog.error(message="test message") + main._handle_exception( + dialog.DialogError, exc_value=exception, trace=None, config=None) + error_msg = mock_sys.exit.call_args_list[-1][0][0] + self.assertTrue("test message" in error_msg) + def test_read_file(self): rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo')) self.assertRaises( From 144dbdd90b0637777163597e338c4872c890e6e3 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 12:23:15 -0700 Subject: [PATCH 079/445] Explain whether tests succeeded or failed overall --- .../certbot_compatibility_test/test_driver.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 165791684..fae2ed2c7 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -7,6 +7,7 @@ import os import shutil import tempfile import time +import sys import OpenSSL @@ -341,6 +342,7 @@ def main(): temp_dir = tempfile.mkdtemp() plugin = PLUGINS[args.plugin](args) try: + overall_success = True while plugin.has_more_configs(): success = True @@ -359,10 +361,18 @@ def main(): if success: logger.info("All tests on %s succeeded", config) else: + overall_success = False logger.error("Tests on %s failed", config) finally: plugin.cleanup_from_tests() + if overall_success: + logger.warn("All compatibility tests succeeded") + sys.exit(0) + else: + logger.warn("One or more compatibility tests failed") + sys.exit(1) + if __name__ == "__main__": main() From 8723bded727a9901314187427ab0e07fbd43b586 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:17:11 -0700 Subject: [PATCH 080/445] Add extra saves for apache plugin --- .../certbot_compatibility_test/test_driver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index fae2ed2c7..38abffb18 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -144,6 +144,7 @@ def test_deploy_cert(plugin, temp_dir, domains): for domain in domains: try: plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path) + plugin.save() # Needed by the Apache plugin except le_errors.Error as error: logger.error("Plugin failed to deploy ceritificate for %s:", domain) logger.exception(error) @@ -178,6 +179,7 @@ def test_enhancements(plugin, domains): for domain in domains: try: plugin.enhance(domain, "redirect") + plugin.save() # Needed by the Apache plugin except le_errors.PluginError as error: # Don't immediately fail because a redirect may already be enabled logger.warning("Plugin failed to enable redirect for %s:", domain) From 1c1816fb4a49e97b99300d50be00df5f41bb508d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:35:28 -0700 Subject: [PATCH 081/445] Update tarball --- .../testdata/configs.tar.gz | Bin 101287 -> 100286 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz index 7323acf747643887d8883f3ef74437d811032b2d..05f7f4f9bc54975c0f575ebd80613c9e419dc3b2 100644 GIT binary patch literal 100286 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

>+7bOwi=X@0cTd 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 z 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 literal 101287 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

>PD40$S_DOkt_8cFeaIjOMKd6SRvO4frMW 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 YDNSigQ<~D0rhMh{f548beE?_+0L2q{f&c&j From 5f8964a6a854ba3f1afd002b3062f52d0eea2aee Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 6 Jun 2016 14:48:49 -0700 Subject: [PATCH 082/445] Fix typo --- .../certbot_compatibility_test/configurators/apache/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py index 918db5f47..ed3d9d67a 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/apache/common.py @@ -118,7 +118,7 @@ def _get_server_root(config): if os.path.isdir(os.path.join(config, name))] if len(subdirs) != 1: - errors.Error("Malformed configuration directiory {0}".format(config)) + errors.Error("Malformed configuration directory {0}".format(config)) return os.path.join(config, subdirs[0].rstrip()) From e826de5db70823594d2c9c286a953de5fc656c11 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:52:30 -0700 Subject: [PATCH 083/445] Don't change line endings on a tarball --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 5eee84cce..8c41b686e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ *.jpeg binary *.jpg binary *.png binary +*.gz binary From ce378cec216d73c8f20ee3d34981089a1d842829 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 6 Jun 2016 14:53:12 -0700 Subject: [PATCH 084/445] Try updating tarball again --- .../testdata/configs.tar.gz | Bin 100286 -> 100288 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz b/certbot-compatibility-test/certbot_compatibility_test/testdata/configs.tar.gz index 05f7f4f9bc54975c0f575ebd80613c9e419dc3b2..9b819d0c7b2d8178d3aad1bb25aeb32a4efae76c 100644 GIT binary patch delta 31 lcmdnj&vu}njZMCrgJIK$jcg)vjJ%trCjhE{3hDp= delta 27 jcmX@m&$h3hjZMCrgJH+(jcg)vo2BLA7H+QE?{)$JjvETn From 0db7b5998b8abb5c70cc13bf2ed61b671fcdef0c Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 6 Jun 2016 15:15:52 -0700 Subject: [PATCH 085/445] fix broken link in contributing.rst --- docs/contributing.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 3318ec103..267d466e4 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -266,8 +266,7 @@ with the core upstream source code. An example is provided in it with any necessary API changes. .. _`setuptools entry points`: - https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins - + http://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points .. _coding-style: From 092173c60892ee79d50fad19fc1ff6b05a27b852 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 6 Jun 2016 17:05:51 -0700 Subject: [PATCH 086/445] fix broken link in contributing.rst (#3130) --- docs/contributing.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 3318ec103..267d466e4 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -266,8 +266,7 @@ with the core upstream source code. An example is provided in it with any necessary API changes. .. _`setuptools entry points`: - https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins - + http://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points .. _coding-style: From ac220976f1032a26d1801da2528ffd554e0e400d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 7 Jun 2016 14:46:49 -0700 Subject: [PATCH 087/445] work in progress --- acme/acme/errors.py | 2 +- certbot-nginx/certbot_nginx/nginxparser.py | 52 ++++++++++++++++++---- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/acme/acme/errors.py b/acme/acme/errors.py index 77d47c522..ca2ab1874 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -49,7 +49,7 @@ class MissingNonce(NonceError): def __str__(self): return ('Server {0} response did not include a replay ' - 'nonce, headers: {1}'.format( + 'nonce, headers: {1}\n(This may be a service outage)'.format( self.response.request.method, self.response.headers)) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index f9348398a..7b39234dc 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -53,8 +53,44 @@ class RawNginxParser(object): """Returns the parsed tree as a list.""" return self.parse().asList() - class RawNginxDumper(object): + # pylint: disable=too-few-public-methods + """A class that dumps nginx configuration from the provided tree.""" + def __init__(self, blocks, indentation=0): + self.blocks = blocks + self.indentation = indentation + + def __iter__(self, blocks=None, current_indent=0, spacer=''): + """Iterates the dumped nginx content.""" + blocks = blocks or self.blocks + for key, values in blocks.spaced: + #indentation = spacer * current_indent + if isinstance(key, list): + yield "".join(key) + ' {' + + for parameter in values: + dumped = self.__iter__([parameter], current_indent + self.indentation) + for line in dumped: + yield line + + yield '}' + else: + if key == '#': + yield key + values + else: + if values is None: + yield key + ';' + else: + yield key + values + ';' + + def __str__(self): + """Return the parsed block as a string.""" + return '\n'.join(self) + '\n' + + + + +class OldRawNginxDumper(object): # pylint: disable=too-few-public-methods """A class that dumps nginx configuration from the provided tree.""" def __init__(self, blocks, indentation=4): @@ -102,7 +138,7 @@ def loads(source): :rtype: list """ - return RawNginxParser(source).as_list() + return UnspacedList(RawNginxParser(source).as_list()) def load(_file): @@ -113,13 +149,13 @@ def load(_file): :rtype: list """ - return loads(_file.read()) + return UnspacedList(loads(_file.read())) def dumps(blocks, indentation=4): """Dump to a string. - :param list block: The parsed tree + :param UnspacedList block: The parsed tree :param int indentation: The number of spaces to indent :rtype: str @@ -130,7 +166,7 @@ def dumps(blocks, indentation=4): def dump(blocks, _file, indentation=4): """Dump to a file. - :param list block: The parsed tree + :param UnspacedList block: The parsed tree :param file _file: The file to dump to :param int indentation: The number of spaces to indent :rtype: NoneType @@ -149,14 +185,14 @@ class UnspacedList(list): self.spaced = copy.deepcopy(list(list_source)) # Turn self into a version of the source list that has spaces removed - # and all sub-lists also UnspaceList()ed + # and all sub-lists also UnspacedList()ed list.__init__(self, list_source) for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): list.__setitem__(self, i, UnspacedList(entry)) elif spacey(entry): list.__delitem__(self, i) - + def insert(self, i, x): self.spaced.insert(i + self._spaces_before(i), x) list.insert(self, i, x) @@ -176,7 +212,7 @@ class UnspacedList(list): else: self.spaced.__add__(other) list.__add__(self, other) - + def __setitem__(self, i, value): self.spaced.__setitem__(i + self._spaces_before(i), value) list.__setitem__(self, i, value) From 8aa1d85991daa33dc22172f4535ed65606299a85 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 7 Jun 2016 16:25:08 -0700 Subject: [PATCH 088/445] Move mageia bootstrap script --- .../pieces/bootstrappers/mageia_common.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bootstrap/_mageia_common.sh => letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh (100%) diff --git a/bootstrap/_mageia_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh similarity index 100% rename from bootstrap/_mageia_common.sh rename to letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh From 1c363716a086ec4dfb3b0c3143e915a75efd106f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 7 Jun 2016 16:33:04 -0700 Subject: [PATCH 089/445] Wrap mageia bootstrap script in bash function --- .../pieces/bootstrappers/mageia_common.sh | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) mode change 100755 => 100644 letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh old mode 100755 new mode 100644 index 9a4606c9d..d6651574a --- a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh @@ -1,24 +1,23 @@ -#!/bin/sh +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi -# Tested on mageia 5 x86_64 -if ! urpmi --force \ - python \ - libpython-devel \ - python-virtualenv -then - echo "Could not install Python dependencies. Aborting bootstrap!" - exit 1 -fi - -if ! urpmi --force \ - git \ - gcc \ - cdialog \ - python-augeas \ - libopenssl-devel \ - libffi-devel \ - rootcerts -then - echo "Could not install additional dependencies. Aborting bootstrap!" - exit 1 -fi + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} From 9be5f7d7d961cf1def5872acec9cb9bc41e11016 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 7 Jun 2016 17:17:17 -0700 Subject: [PATCH 090/445] Further WIP --- certbot-nginx/certbot_nginx/nginxparser.py | 99 +++++++++++++++++++--- certbot-nginx/certbot_nginx/parser.py | 2 +- 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 7b39234dc..8c2ba197e 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -1,6 +1,7 @@ """Very low-level nginx config parser based on pyparsing.""" import copy import string +import sys from pyparsing import ( Literal, White, Word, alphanums, CharsNotIn, Forward, Group, @@ -18,6 +19,7 @@ class RawNginxParser(object): right_bracket = Literal("}").suppress() semicolon = Literal(";").suppress() space = White().suppress() + keepSpace = Optional(White()) key = Word(alphanums + "_/+-.") # Matches anything that is not a special character AND any chars in single # or double quotes @@ -27,8 +29,9 @@ class RawNginxParser(object): modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") # rules - comment = White() + Literal('#') + restOfLine() - assignment = White() + key + Optional(space + value, default=None) + semicolon + comment = Literal('#') + restOfLine() + + assignment = keepSpace + key + Optional(space + value, default=None) + semicolon location_statement = Optional(space + modifier) + Optional(space + location) if_statement = Literal("if") + space + Regex(r"\(.+\)") + space map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space @@ -53,6 +56,51 @@ class RawNginxParser(object): """Returns the parsed tree as a list.""" return self.parse().asList() +class OldRawNginxParser(object): + # pylint: disable=expression-not-assigned + """A class that parses nginx configuration with pyparsing.""" + + # constants + left_bracket = Literal("{").suppress() + right_bracket = Literal("}").suppress() + semicolon = Literal(";").suppress() + space = White().suppress() + key = Word(alphanums + "_/+-.") + # 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 + comment = Literal("#") + restOfLine() + assignment = (key + Optional(space + value, default=None) + semicolon) + location_statement = Optional(space + modifier) + Optional(space + location) + if_statement = Literal("if") + space + Regex(r"\(.+\)") + space + map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space + block = Forward() + + block << Group( + (Group(key + location_statement) ^ Group(if_statement) ^ Group(map_statement)) + + left_bracket + + Group(ZeroOrMore(Group(comment | assignment) | block)) + + right_bracket) + + script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd + + def __init__(self, source): + self.source = source + + def parse(self): + """Returns the parsed tree.""" + return self.script.parseString(self.source) + + def as_list(self): + """Returns the parsed tree as a list.""" + return self.parse().asList() + + class RawNginxDumper(object): # pylint: disable=too-few-public-methods """A class that dumps nginx configuration from the provided tree.""" @@ -60,13 +108,24 @@ class RawNginxDumper(object): self.blocks = blocks self.indentation = indentation - def __iter__(self, blocks=None, current_indent=0, spacer=''): + def __iter__(self, blocks=None, current_indent=0, spacer=' '): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks - for key, values in blocks.spaced: + print "iterating", blocks + for b in blocks: + if len(b) == 2: + key, values = b + indentation = "" + elif len(b) == 3: + indentation, key, values = b + assert indentation.isspace(), indentation + " is not space" + yield indentation + else: + print "Cannot process", b + sys.exit(1) #indentation = spacer * current_indent if isinstance(key, list): - yield "".join(key) + ' {' + yield spacer.join(key) + ' {' for parameter in values: dumped = self.__iter__([parameter], current_indent + self.indentation) @@ -75,13 +134,13 @@ class RawNginxDumper(object): yield '}' else: - if key == '#': + if isinstance(key, str) and key.strip() == '#': yield key + values else: if values is None: yield key + ';' else: - yield key + values + ';' + yield key + spacer + values + ';' def __str__(self): """Return the parsed block as a string.""" @@ -138,7 +197,28 @@ def loads(source): :rtype: list """ - return UnspacedList(RawNginxParser(source).as_list()) + old = OldRawNginxParser(source).as_list() + print "Old:" + for entry in old: + print len(entry), " ", + new = UnspacedList(RawNginxParser(source).as_list()) + print "\nNew:" + print new + for entry in new: + print len(entry), " ", + print "\nNewspaced:" + for entry in new.spaced: + print str(len(entry))+ " ", + print "\ngo" + if old != new: + print "NON-MATCH" + for a, b in zip(old, new): + if a != b: + print "Entry", a, "!=", b + import sys + else: + print "Parallel" + return new def load(_file): @@ -149,7 +229,7 @@ def load(_file): :rtype: list """ - return UnspacedList(loads(_file.read())) + return loads(_file.read()) def dumps(blocks, indentation=4): @@ -175,7 +255,6 @@ def dump(blocks, _file, indentation=4): return _file.write(dumps(blocks, indentation)) - spacey = lambda x: isinstance(x, str) and x.isspace() class UnspacedList(list): diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 2f08c15d3..2872654b5 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -209,7 +209,7 @@ class NginxParser(object): """ for filename in self.parsed: - tree = self.parsed[filename] + tree = self.parsed[filename].spaced if ext: filename = filename + os.path.extsep + ext try: From e51c16d666655c2c2405ea604c5aee76bf5eb4ca Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 7 Jun 2016 17:24:56 -0700 Subject: [PATCH 091/445] Update letsencrypt-auto changes for the new format --- letsencrypt-auto-source/letsencrypt-auto | 27 +++++++++++++++++++ .../letsencrypt-auto.template | 4 +++ 2 files changed, 31 insertions(+) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..eef0c957d 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -458,12 +458,39 @@ BootstrapSmartOS() { pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' } +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} + # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 43d8bc7e1..4d15d281d 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -155,12 +155,16 @@ DeterminePythonVersion() { {{ bootstrappers/free_bsd.sh }} {{ bootstrappers/mac.sh }} {{ bootstrappers/smartos.sh }} +{{ bootstrappers/mageia_common.sh }} # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon From 80a52d8f018c99f186d6d7642edc86294852702d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 7 Jun 2016 17:55:53 -0700 Subject: [PATCH 092/445] Vaguely close to working? --- certbot-nginx/certbot_nginx/nginxparser.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 8c2ba197e..9e7136ced 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -31,7 +31,7 @@ class RawNginxParser(object): # rules comment = Literal('#') + restOfLine() - assignment = keepSpace + key + Optional(space + value, default=None) + semicolon + assignment = keepSpace + key + Optional(White() + value, default=None) + semicolon location_statement = Optional(space + modifier) + Optional(space + location) if_statement = Literal("if") + space + Regex(r"\(.+\)") + space map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space @@ -119,14 +119,13 @@ class RawNginxDumper(object): elif len(b) == 3: indentation, key, values = b assert indentation.isspace(), indentation + " is not space" - yield indentation + indentation = indentation.replace("\n", "", 1) else: print "Cannot process", b sys.exit(1) #indentation = spacer * current_indent if isinstance(key, list): - yield spacer.join(key) + ' {' - + yield indentation + spacer.join(key) + ' {' for parameter in values: dumped = self.__iter__([parameter], current_indent + self.indentation) for line in dumped: @@ -135,15 +134,16 @@ class RawNginxDumper(object): yield '}' else: if isinstance(key, str) and key.strip() == '#': - yield key + values + yield indentation + key + values else: if values is None: - yield key + ';' + yield indentation + key + ';' else: - yield key + spacer + values + ';' + yield indentation + key + spacer + values + ';' def __str__(self): """Return the parsed block as a string.""" + print "Merging:\n", '\n'.join(self) return '\n'.join(self) + '\n' From 443f2ebb580faf6a582efe359405c4180f7ee0a6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 7 Jun 2016 18:19:58 -0700 Subject: [PATCH 093/445] WIP --- certbot-nginx/certbot_nginx/nginxparser.py | 23 ++++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 9e7136ced..2a68de77b 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -31,7 +31,7 @@ class RawNginxParser(object): # rules comment = Literal('#') + restOfLine() - assignment = keepSpace + key + Optional(White() + value, default=None) + semicolon + assignment = keepSpace + key + Optional(space + value, default=None) + semicolon location_statement = Optional(space + modifier) + Optional(space + location) if_statement = Literal("if") + space + Regex(r"\(.+\)") + space map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space @@ -108,31 +108,28 @@ class RawNginxDumper(object): self.blocks = blocks self.indentation = indentation - def __iter__(self, blocks=None, current_indent=0, spacer=' '): + def __iter__(self, blocks=None, spacer=' '): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks print "iterating", blocks for b in blocks: - if len(b) == 2: - key, values = b - indentation = "" - elif len(b) == 3: - indentation, key, values = b - assert indentation.isspace(), indentation + " is not space" + indentation = "" + if spacey(b[0]): + indentation = b.pop(0) indentation = indentation.replace("\n", "", 1) - else: - print "Cannot process", b - sys.exit(1) - #indentation = spacer * current_indent + key = b.pop(0) + + values = b if isinstance(key, list): yield indentation + spacer.join(key) + ' {' for parameter in values: - dumped = self.__iter__([parameter], current_indent + self.indentation) + dumped = self.__iter__([parameter]) for line in dumped: yield line yield '}' else: + #yield b if isinstance(key, str) and key.strip() == '#': yield indentation + key + values else: From 96dd662e558da73d178dd4fef0f14a5f1dc67975 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 14:35:59 -0700 Subject: [PATCH 094/445] Delint certbot-compatibility-test --- .../certbot_compatibility_test/configurators/common.py | 1 - certbot-compatibility-test/certbot_compatibility_test/util.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py index 4592eca39..03128cc86 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/common.py @@ -5,7 +5,6 @@ import shutil import tempfile from certbot import constants -from certbot_compatibility_test import errors from certbot_compatibility_test import util diff --git a/certbot-compatibility-test/certbot_compatibility_test/util.py b/certbot-compatibility-test/certbot_compatibility_test/util.py index 570bf1a9e..af951aa6a 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/util.py +++ b/certbot-compatibility-test/certbot_compatibility_test/util.py @@ -1,11 +1,9 @@ """Utility functions for Certbot plugin tests.""" import argparse import copy -import contextlib import os import re import shutil -import socket import tarfile from acme import jose From a0be028340acf1de55426a48ee4e89994f5a4166 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:49:08 -0700 Subject: [PATCH 095/445] Add _get_names_from_cert_or_req --- certbot/crypto_util.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 6b1b8426c..8640fec1e 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -296,6 +296,18 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): csr, OpenSSL.crypto.load_certificate_request, typ) +def _get_names_from_cert_or_req(cert_or_req, load_func, typ): + loaded_cert_or_req = _load_cert_or_req(cert_or_req, load_func, typ) + subject = loaded_cert_or_req.get_subject().CN + # pylint: disable=protected-access + sans = acme_crypto_util._pyopenssl_cert_or_req_san(loaded_cert_or_req) + + if subject is None: + return sans + else: + return [subject] + [d for d in sans if d != subject] + + def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): """Get a list of domains from a CSR, including the CN if it is set. From ac581951b3926c590ce930230a315d31e91ed0b2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:50:34 -0700 Subject: [PATCH 096/445] Have get_names_from_csr use _get_names_from_cert_or_req --- certbot/crypto_util.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 8640fec1e..e8047f086 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -318,13 +318,8 @@ def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): :rtype: list """ - loaded_csr = _load_cert_or_req( + return _get_names_from_cert_or_req( csr, OpenSSL.crypto.load_certificate_request, typ) - # Use a set to avoid duplication with CN and Subject Alt Names - domains = set(d for d in (loaded_csr.get_subject().CN,) if d is not None) - # pylint: disable=protected-access - domains.update(acme_crypto_util._pyopenssl_cert_or_req_san(loaded_csr)) - return list(domains) def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): From 753aea2f3f495eccb3f50cf977c3864dc56a55b6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:53:04 -0700 Subject: [PATCH 097/445] Add get_names_from_cert function --- certbot/crypto_util.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index e8047f086..f45645de1 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -308,6 +308,20 @@ def _get_names_from_cert_or_req(cert_or_req, load_func, typ): return [subject] + [d for d in sans if d != subject] +def get_names_from_cert(csr, typ=OpenSSL.crypto.FILETYPE_PEM): + """Get a list of domains from a cert, including the CN if it is set. + + :param str cert: Certificate (encoded). + :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` + + :returns: A list of domain names. + :rtype: list + + """ + return _get_names_from_cert_or_req( + csr, OpenSSL.crypto.load_certificate, typ) + + def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): """Get a list of domains from a CSR, including the CN if it is set. From 8db1b5627c02d4a02890075e1a3ca6f9a3a29d96 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:57:56 -0700 Subject: [PATCH 098/445] Add GetNamesFromCertTest --- certbot/tests/crypto_util_test.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index fa88e89e7..742d4ec8c 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -273,6 +273,25 @@ class GetSANsFromCSRTest(unittest.TestCase): [], self._call(test_util.load_vector('csr-nosans.pem'))) +class GetNamesFromCertTest(unittest.TestCase): + """Tests for certbot.crypto_util.get_names_from_cert.""" + + @classmethod + def _call(cls, *args, **kwargs): + from certbot.crypto_util import get_names_from_cert + return get_names_from_cert(*args, **kwargs) + + def test_single(self): + self.assertEqual( + ['example.com'], + self._call(test_util.load_vector('cert.pem'))) + + def test_san(self): + self.assertEqual( + ['example.com', 'www.example.com'], + self._call(test_util.load_vector('cert-san.pem'))) + + class GetNamesFromCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.get_names_from_csr.""" @classmethod From 0a707b64ec0b9a0ee61fe56fc6bcf4b1b9525318 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 16:59:44 -0700 Subject: [PATCH 099/445] Use common_name instead of subject --- certbot/crypto_util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index f45645de1..1e831dd8f 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -298,14 +298,14 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): def _get_names_from_cert_or_req(cert_or_req, load_func, typ): loaded_cert_or_req = _load_cert_or_req(cert_or_req, load_func, typ) - subject = loaded_cert_or_req.get_subject().CN + common_name = loaded_cert_or_req.get_subject().CN # pylint: disable=protected-access sans = acme_crypto_util._pyopenssl_cert_or_req_san(loaded_cert_or_req) - if subject is None: + if common_name is None: return sans else: - return [subject] + [d for d in sans if d != subject] + return [common_name] + [d for d in sans if d != common_name] def get_names_from_cert(csr, typ=OpenSSL.crypto.FILETYPE_PEM): From 2c803eff6ab161079128dca16f8efd8154049ac1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 8 Jun 2016 17:01:54 -0700 Subject: [PATCH 100/445] Use get_names_from_cert in storage.py --- certbot/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/storage.py b/certbot/storage.py index b0c8245d3..60886e306 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -616,7 +616,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes if target is None: raise errors.CertStorageError("could not find cert file") with open(target) as f: - return crypto_util.get_sans_from_cert(f.read()) + return crypto_util.get_names_from_cert(f.read()) def autodeployment_is_enabled(self): """Is automatic deployment enabled for this cert? From 8147c671e42c95cd7aa63f15f2d8bb3cf280c0bd Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 8 Jun 2016 17:52:35 -0700 Subject: [PATCH 101/445] Now handles some conf files in whitespace-preserving mode (but not all of them) --- certbot-nginx/certbot_nginx/nginxparser.py | 136 ++++----------------- certbot-nginx/certbot_nginx/parser.py | 22 +++- certbot-nginx/certbot_nginx/tls_sni_01.py | 9 +- 3 files changed, 53 insertions(+), 114 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 2a68de77b..13498bee2 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -18,8 +18,7 @@ class RawNginxParser(object): left_bracket = Literal("{").suppress() right_bracket = Literal("}").suppress() semicolon = Literal(";").suppress() - space = White().suppress() - keepSpace = Optional(White()) + space = Optional(White()) key = Word(alphanums + "_/+-.") # Matches anything that is not a special character AND any chars in single # or double quotes @@ -29,21 +28,23 @@ class RawNginxParser(object): modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") # rules - comment = Literal('#') + restOfLine() + comment = space + Literal('#') + restOfLine() - assignment = keepSpace + key + Optional(space + value, default=None) + semicolon - location_statement = Optional(space + modifier) + Optional(space + location) - if_statement = Literal("if") + space + Regex(r"\(.+\)") + space - map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space + assignment = space + key + Optional(space + value, default=None) + semicolon + location_statement = space + Optional(modifier) + Optional(space + location) + if_statement = space + Literal("if") + space + Regex(r"\(.+\)") + space + map_statement = space + Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space block = Forward() block << Group( + # XXX could this "key" be Literal("location")? (Group(key + location_statement) ^ Group(if_statement) ^ Group(map_statement)) + left_bracket + Group(ZeroOrMore(Group(comment | assignment) | block)) + right_bracket) script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd + script.parseWithTabs() def __init__(self, source): self.source = source @@ -56,51 +57,6 @@ class RawNginxParser(object): """Returns the parsed tree as a list.""" return self.parse().asList() -class OldRawNginxParser(object): - # pylint: disable=expression-not-assigned - """A class that parses nginx configuration with pyparsing.""" - - # constants - left_bracket = Literal("{").suppress() - right_bracket = Literal("}").suppress() - semicolon = Literal(";").suppress() - space = White().suppress() - key = Word(alphanums + "_/+-.") - # 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 - comment = Literal("#") + restOfLine() - assignment = (key + Optional(space + value, default=None) + semicolon) - location_statement = Optional(space + modifier) + Optional(space + location) - if_statement = Literal("if") + space + Regex(r"\(.+\)") + space - map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space - block = Forward() - - block << Group( - (Group(key + location_statement) ^ Group(if_statement) ^ Group(map_statement)) + - left_bracket + - Group(ZeroOrMore(Group(comment | assignment) | block)) + - right_bracket) - - script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd - - def __init__(self, source): - self.source = source - - def parse(self): - """Returns the parsed tree.""" - return self.script.parseString(self.source) - - def as_list(self): - """Returns the parsed tree as a list.""" - return self.parse().asList() - - class RawNginxDumper(object): # pylint: disable=too-few-public-methods """A class that dumps nginx configuration from the provided tree.""" @@ -108,35 +64,41 @@ class RawNginxDumper(object): self.blocks = blocks self.indentation = indentation - def __iter__(self, blocks=None, spacer=' '): + def __iter__(self, blocks=None): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks print "iterating", blocks for b in blocks: + b0 = b + b = copy.deepcopy(b) indentation = "" if spacey(b[0]): indentation = b.pop(0) indentation = indentation.replace("\n", "", 1) key = b.pop(0) + values = b.pop(0) - values = b if isinstance(key, list): - yield indentation + spacer.join(key) + ' {' + yield indentation + "".join(key) + '{' for parameter in values: dumped = self.__iter__([parameter]) for line in dumped: yield line - yield '}' else: - #yield b if isinstance(key, str) and key.strip() == '#': yield indentation + key + values else: + gap = "" + # Sometimes the parser has stuck some gap whitespace in here; + # if so rotate it into gap + if spacey(values): + gap = values + values = b.pop(0) if values is None: - yield indentation + key + ';' + yield indentation + key + gap + ';' else: - yield indentation + key + spacer + values + ';' + yield indentation + key + gap + values + ';' def __str__(self): """Return the parsed block as a string.""" @@ -146,42 +108,6 @@ class RawNginxDumper(object): -class OldRawNginxDumper(object): - # pylint: disable=too-few-public-methods - """A class that dumps nginx configuration from the provided tree.""" - def __init__(self, blocks, indentation=4): - self.blocks = blocks - self.indentation = indentation - - def __iter__(self, blocks=None, current_indent=0, spacer=' '): - """Iterates the dumped nginx content.""" - blocks = blocks or self.blocks - for key, values in blocks: - indentation = spacer * current_indent - if isinstance(key, list): - if current_indent: - yield '' - yield indentation + spacer.join(key) + ' {' - - for parameter in values: - dumped = self.__iter__([parameter], current_indent + self.indentation) - for line in dumped: - yield line - - yield indentation + '}' - else: - if key == '#': - yield spacer * current_indent + key + values - else: - if values is None: - yield spacer * current_indent + key + ';' - else: - yield spacer * current_indent + key + spacer + values + ';' - - def __str__(self): - """Return the parsed block as a string.""" - return '\n'.join(self) + '\n' - # Shortcut functions to respect Python's serialization interface # (like pyyaml, picker or json) @@ -194,10 +120,6 @@ def loads(source): :rtype: list """ - old = OldRawNginxParser(source).as_list() - print "Old:" - for entry in old: - print len(entry), " ", new = UnspacedList(RawNginxParser(source).as_list()) print "\nNew:" print new @@ -207,14 +129,6 @@ def loads(source): for entry in new.spaced: print str(len(entry))+ " ", print "\ngo" - if old != new: - print "NON-MATCH" - for a, b in zip(old, new): - if a != b: - print "Entry", a, "!=", b - import sys - else: - print "Parallel" return new @@ -229,7 +143,7 @@ def load(_file): return loads(_file.read()) -def dumps(blocks, indentation=4): +def dumps(blocks): """Dump to a string. :param UnspacedList block: The parsed tree @@ -237,10 +151,10 @@ def dumps(blocks, indentation=4): :rtype: str """ - return str(RawNginxDumper(blocks, indentation)) + return str(RawNginxDumper(blocks)) -def dump(blocks, _file, indentation=4): +def dump(blocks, _file): """Dump to a file. :param UnspacedList block: The parsed tree @@ -249,7 +163,7 @@ def dump(blocks, _file, indentation=4): :rtype: NoneType """ - return _file.write(dumps(blocks, indentation)) + return _file.write(dumps(blocks)) spacey = lambda x: isinstance(x, str) and x.isspace() diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 2872654b5..642702c9f 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -4,6 +4,7 @@ import logging import os import pyparsing import re +import sys from certbot import errors @@ -213,9 +214,26 @@ class NginxParser(object): if ext: filename = filename + os.path.extsep + ext try: - logger.debug('Dumping to %s:\n%s', filename, nginxparser.dumps(tree)) + out = nginxparser.dumps(tree) + logger.debug('Dumping to %s:\n%s', filename, out) with open(filename, 'w') as _file: - nginxparser.dump(tree, _file) + _file.write(out) + + if "owncloud" in filename: + print "Outputting", filename + print out + a = open("/tmp/nginx/sites-enabled/owncloud.conf").read() + b = open(filename).read() + for linea, lineb in zip(a.split('\n'), b.split('\n')): + if linea != lineb: + print "a", repr(linea) + print "b", repr(lineb) + if a != b: + print "Mismatch!" + if a != out: + print "Double mismatch", len(a), len(out) + else: + print "Match!" except IOError: logger.error("Could not open file for writing: %s", filename) diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index e4c5d31a6..efb5d53e6 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -3,6 +3,7 @@ import itertools import logging import os +import sys from certbot import errors from certbot.plugins import common @@ -123,7 +124,13 @@ class NginxTlsSni01(common.TLSSNI01): True, self.challenge_conf) with open(self.challenge_conf, "w") as new_conf: - nginxparser.dump(config, new_conf) + if "mime" in self.challenge_conf: + print "Weird" + out = nginxparser.dumps(config) + print out + #sys.exit(1) + #nginxparser.dump(config, new_conf) + new_conf.write(out) def _make_server_block(self, achall, addrs): """Creates a server block for a challenge. From afd899886d255f4c15e89ab7940a561083a3d2c1 Mon Sep 17 00:00:00 2001 From: Willem Fibbe Date: Wed, 8 Jun 2016 16:26:56 +0200 Subject: [PATCH 102/445] Prevent bootstrap-issue on Debian systems with virtualenv package On Debian 7 (and probably relative distro's) `aptitude show virtualenv` exits with 0, since it is a virtual package. However, it doesn't have any installation candidates, so filter on this case before trying to install `virtualenv` to prevent installation-errors while bootstrapping. NB, to make this clear: (0)#: apt-cache show virtualenv N: Can't select versions from package 'virtualenv' as it is purely virtual N: No packages found (0)#: echo $? 0 Furthermore, --quiet=0 is necessary, to be able to grep through `apt-cache`'s output via a pipe. More details on http://unix.stackexchange.com/questions/201869/why-isnt-apt-cache-policy-output-piped/202041#202041. --- letsencrypt-auto-source/letsencrypt-auto | 2 +- letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1992c9d47..3ed48216d 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -172,7 +172,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index bfbcfa31d..8eb7e16ee 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -23,7 +23,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi From 02cdb5db0ef2e035a19b86ca0b462a0a38e50694 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 16:03:32 -0700 Subject: [PATCH 103/445] Add cert with not alphabetically first CN --- certbot/tests/testdata/cert-5sans.pem | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 certbot/tests/testdata/cert-5sans.pem diff --git a/certbot/tests/testdata/cert-5sans.pem b/certbot/tests/testdata/cert-5sans.pem new file mode 100644 index 000000000..5de7cc6cb --- /dev/null +++ b/certbot/tests/testdata/cert-5sans.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICkTCCAjugAwIBAgIJAJNbfABWQ8bbMA0GCSqGSIb3DQEBCwUAMHkxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMScwJQYDVQQKDB5FbGVjdHJvbmljIEZyb250aWVyIEZvdW5kYXRpb24xFDAS +BgNVBAMMC2V4YW1wbGUuY29tMB4XDTE2MDYwOTIzMDEzNloXDTE2MDcwOTIzMDEz +NloweTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xJzAlBgNVBAoMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91 +bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANL +ADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE +30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4GlMIGiMB0GA1UdDgQWBBQmz8jt +S9eUsuQlA1gkjwTAdNWXijAfBgNVHSMEGDAWgBQmz8jtS9eUsuQlA1gkjwTAdNWX +ijAMBgNVHRMEBTADAQH/MFIGA1UdEQRLMEmCDWEuZXhhbXBsZS5jb22CDWIuZXhh +bXBsZS5jb22CDWMuZXhhbXBsZS5jb22CDWQuZXhhbXBsZS5jb22CC2V4YW1wbGUu +Y29tMA0GCSqGSIb3DQEBCwUAA0EAVXmZxB+IJdgFvY2InOYeytTD1QmouDZRtj/T +H/HIpSdsfO7qr4d/ZprI2IhLRxp2S4BiU5Qc5HUkeADcpNd06A== +-----END CERTIFICATE----- From 4f99cc7b2a2f1eb4eb1820e75d909551eea2d1c7 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:43:05 -0700 Subject: [PATCH 104/445] Add _write_out_kind method --- certbot/tests/storage_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 0c88d3d55..0579c9f1c 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -84,6 +84,16 @@ class BaseRenewableCertTest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.tempdir) + def _write_out_kind(self, kind, ver, value=None): + link = getattr(self.test_rc, kind) + if os.path.lexists(link): + os.unlink(link) + os.symlink(os.path.join(os.path.pardir, os.path.pardir, "archive", + "example.org", "{0}{1}.pem".format(kind, ver)), + link) + with open(link, "w") as f: + f.write(kind if value is None else value) + def _write_out_ex_kinds(self): for kind in ALL_FOUR: where = getattr(self.test_rc, kind) From 562802bfd0443f14cd928ee41a4ae73b9453ce39 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:44:33 -0700 Subject: [PATCH 105/445] Refactor common symlink writing code --- certbot/tests/storage_test.py | 124 ++++++---------------------------- 1 file changed, 19 insertions(+), 105 deletions(-) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 0579c9f1c..44b881fd9 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -96,16 +96,8 @@ class BaseRenewableCertTest(unittest.TestCase): def _write_out_ex_kinds(self): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}12.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}11.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 12) + self._write_out_kind(kind, 11) class RenewableCertTests(BaseRenewableCertTest): @@ -214,10 +206,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_current_target(self): # Relative path logic - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert17.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") + self._write_out_kind("cert", 17) self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"), os.path.join(self.tempdir, "archive", "example.org", @@ -235,12 +224,8 @@ class RenewableCertTests(BaseRenewableCertTest): def test_current_version(self): for ver in (1, 5, 10, 20): - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert{0}.pem".format(ver)), - self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") - os.unlink(self.test_rc.cert) + self._write_out_kind("cert", ver) + os.unlink(self.test_rc.cert) os.symlink(os.path.join("..", "..", "archive", "example.org", "cert10.pem"), self.test_rc.cert) self.assertEqual(self.test_rc.current_version("cert"), 10) @@ -251,61 +236,30 @@ class RenewableCertTests(BaseRenewableCertTest): def test_latest_and_next_versions(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(self.test_rc.latest_common_version(), 5) self.assertEqual(self.test_rc.next_free_version(), 6) # Having one kind of file of a later version doesn't change the # result - os.unlink(self.test_rc.privkey) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "privkey7.pem"), self.test_rc.privkey) - with open(self.test_rc.privkey, "w") as f: - f.write("privkey") + self._write_out_kind("privkey", 7) self.assertEqual(self.test_rc.latest_common_version(), 5) # ... although it does change the next free version self.assertEqual(self.test_rc.next_free_version(), 8) # Nor does having three out of four change the result - os.unlink(self.test_rc.cert) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert7.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") - os.unlink(self.test_rc.fullchain) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "fullchain7.pem"), self.test_rc.fullchain) - with open(self.test_rc.fullchain, "w") as f: - f.write("fullchain") + self._write_out_kind("cert", 7) + self._write_out_kind("fullchain", 7) self.assertEqual(self.test_rc.latest_common_version(), 5) # If we have everything from a much later version, it does change # the result - ver = 17 for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 17) self.assertEqual(self.test_rc.latest_common_version(), 17) self.assertEqual(self.test_rc.next_free_version(), 18) def test_update_link_to(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) # pylint: disable=protected-access self.test_rc._update_link_to("cert", 3) @@ -322,10 +276,7 @@ class RenewableCertTests(BaseRenewableCertTest): "chain3000.pem") def test_version(self): - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert12.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write("cert") + self._write_out_kind("cert", 12) # TODO: We should probably test that the directory is still the # same, but it's tricky because we can get an absolute # path out when we put a relative path in. @@ -335,13 +286,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_update_all_links_to_success(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) self.assertEqual(self.test_rc.latest_common_version(), 5) for ver in xrange(1, 6): @@ -386,13 +331,7 @@ class RenewableCertTests(BaseRenewableCertTest): def test_has_pending_deployment(self): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertEqual(ver, self.test_rc.current_version(kind)) for ver in xrange(1, 6): self.test_rc.update_all_links_to(ver) @@ -405,21 +344,12 @@ class RenewableCertTests(BaseRenewableCertTest): def test_names(self): # Trying the current version - test_cert = test_util.load_vector("cert-san.pem") - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert12.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write(test_cert) + self._write_out_kind("cert", 12, test_util.load_vector("cert-san.pem")) self.assertEqual(self.test_rc.names(), ["example.com", "www.example.com"]) # Trying a non-current version - test_cert = test_util.load_vector("cert.pem") - os.unlink(self.test_rc.cert) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert15.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write(test_cert) + self._write_out_kind("cert", 15, test_util.load_vector("cert.pem")) self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) @@ -490,13 +420,7 @@ class RenewableCertTests(BaseRenewableCertTest): # No pending deployment for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.assertFalse(self.test_rc.should_autodeploy()) def test_autorenewal_is_enabled(self): @@ -517,11 +441,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(self.test_rc.should_autorenew()) self.test_rc.configuration["autorenew"] = "1" for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}12.pem".format(kind)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, 12) # Mandatory renewal on the basis of OCSP revocation mock_ocsp.return_value = True self.assertTrue(self.test_rc.should_autorenew()) @@ -535,13 +455,7 @@ class RenewableCertTests(BaseRenewableCertTest): for ver in xrange(1, 6): for kind in ALL_FOUR: - where = getattr(self.test_rc, kind) - if os.path.islink(where): - os.unlink(where) - os.symlink(os.path.join("..", "..", "archive", "example.org", - "{0}{1}.pem".format(kind, ver)), where) - with open(where, "w") as f: - f.write(kind) + self._write_out_kind(kind, ver) self.test_rc.update_all_links_to(3) self.assertEqual( 6, self.test_rc.save_successor(3, "new cert", None, From bb1d2c0a1faf7d58249e73fdbd6ea832335755b3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:49:44 -0700 Subject: [PATCH 106/445] Test common name is listed first in storage.py --- certbot/tests/storage_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 44b881fd9..0d907eca3 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -353,6 +353,13 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) + # Testing common name is listed first + self._write_out_kind( + "cert", 12, test_util.load_vector("cert-5sans.pem")) + self.assertEqual( + self.test_rc.names(12), + ["example.com"] + ["{0}.example.com".format(c) for c in "abcd"]) + # Trying missing cert os.unlink(self.test_rc.cert) self.assertRaises(errors.CertStorageError, self.test_rc.names) From 07cf34284eabf129ce0fae76d79939d38a8bbcdb Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 9 Jun 2016 17:55:46 -0700 Subject: [PATCH 107/445] Add test_common_name_sans_order --- certbot/tests/crypto_util_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 742d4ec8c..5a592bbb1 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -291,6 +291,13 @@ class GetNamesFromCertTest(unittest.TestCase): ['example.com', 'www.example.com'], self._call(test_util.load_vector('cert-san.pem'))) + def test_common_name_sans_order(self): + # Tests that the common name comes first + # followed by the SANS in alphabetical order + self.assertEqual( + ['example.com'] + ['{0}.example.com'.format(c) for c in 'abcd'], + self._call(test_util.load_vector('cert-5sans.pem'))) + class GetNamesFromCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.get_names_from_csr.""" From bc50ad48c6af5ff1b392dd1379a1cda6fe2d7c04 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 10 Jun 2016 06:02:50 +0200 Subject: [PATCH 108/445] Changing default logging level --- certbot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/constants.py b/certbot/constants.py index 1d4efe80e..fb278161d 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -18,7 +18,7 @@ CLI_DEFAULTS = dict( os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"), "letsencrypt", "cli.ini"), ], - verbose_count=-(logging.WARNING / 10), + verbose_count=-(logging.INFO / 10), server="https://acme-v01.api.letsencrypt.org/directory", rsa_key_size=2048, rollback_checkpoints=1, From b51280875081c868698a2a4b8603c1a9f9905926 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 10 Jun 2016 06:15:03 +0200 Subject: [PATCH 109/445] Changing CLI logging format --- certbot/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index fa14bbf99..bb65680af 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -615,11 +615,12 @@ def _cli_log_handler(config, level, fmt): def setup_logging(config, cli_handler_factory, logfile): """Setup logging.""" - fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" + file_fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" + cli_fmt = "%(message)s" level = -config.verbose_count * 10 file_handler, log_file_path = setup_log_file_handler( - config, logfile=logfile, fmt=fmt) - cli_handler = cli_handler_factory(config, level, fmt) + config, logfile=logfile, fmt=file_fmt) + cli_handler = cli_handler_factory(config, level, cli_fmt) # TODO: use fileConfig? From 7f9a8f0f089a21769aa80bfd6696cdb19f7f5fa0 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 10 Jun 2016 06:26:33 +0200 Subject: [PATCH 110/445] Adjusting logging level of certain messages --- certbot/client.py | 2 +- certbot/main.py | 2 +- certbot/renewal.py | 6 +++--- certbot/reporter.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/certbot/client.py b/certbot/client.py index 1dec6a1a9..81b7ccdc6 100644 --- a/certbot/client.py +++ b/certbot/client.py @@ -282,7 +282,7 @@ class Client(object): "by your operating system package manager") if self.config.dry_run: - logger.info("Dry run: Skipping creating new lineage for %s", + logger.debug("Dry run: Skipping creating new lineage for %s", domains[0]) return None else: diff --git a/certbot/main.py b/certbot/main.py index bb65680af..c19375c32 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -525,7 +525,7 @@ def _csr_obtain_cert(config, le_client): csr, typ = config.actual_csr certr, chain = le_client.obtain_certificate_from_csr(config.domains, csr, typ) if config.dry_run: - logger.info( + logger.debug( "Dry run: skipping saving certificate to %s", config.cert_path) else: cert_path, _, cert_fullchain = le_client.save_certificate( diff --git a/certbot/renewal.py b/certbot/renewal.py index d04e2d27c..059fd6cd9 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -107,7 +107,7 @@ def _restore_webroot_config(config, renewalparams): if not cli.set_by_cli("webroot_map"): config.namespace.webroot_map = renewalparams["webroot_map"] elif "webroot_path" in renewalparams: - logger.info("Ancient renewal conf file without webroot-map, restoring webroot-path") + logger.debug("Ancient renewal conf file without webroot-map, restoring webroot-path") wp = renewalparams["webroot_path"] if isinstance(wp, str): # prior to 0.1.0, webroot_path was a string wp = [wp] @@ -193,7 +193,7 @@ def _restore_required_config_elements(config, renewalparams): def should_renew(config, lineage): "Return true if any of the circumstances for automatic renewal apply." if config.renew_by_default: - logger.info("Auto-renewal forced with --force-renewal...") + logger.debug("Auto-renewal forced with --force-renewal...") return True if lineage.should_autorenew(interactive=True): logger.info("Cert is due for renewal, auto-renewing...") @@ -235,7 +235,7 @@ def renew_cert(config, domains, le_client, lineage): _avoid_invalidating_lineage(config, lineage, original_server) new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains) if config.dry_run: - logger.info("Dry run: skipping updating lineage at %s", + logger.debug("Dry run: skipping updating lineage at %s", os.path.dirname(lineage.cert)) else: prior_version = lineage.latest_common_version() diff --git a/certbot/reporter.py b/certbot/reporter.py index e07011aee..118b13166 100644 --- a/certbot/reporter.py +++ b/certbot/reporter.py @@ -58,7 +58,7 @@ class Reporter(object): """ assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY self.messages.put(self._msg_type(priority, msg, on_crash)) - logger.info("Reporting to user: %s", msg) + logger.debug("Reporting to user: %s", msg) def atexit_print_messages(self, pid=None): """Function to be registered with atexit to print messages. From 6a53522a6cbda3819bba0fd93072c50ef8095869 Mon Sep 17 00:00:00 2001 From: Sergey Nuzdhin Date: Mon, 13 Jun 2016 14:43:47 +0200 Subject: [PATCH 111/445] Add additional warning with actual exception message during renewal Log and show warning with real exception message to make it more clear what exactly happened. Currently we show `config is broken` when in fact we have broken symlinks in live folder. --- certbot/renewal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot/renewal.py b/certbot/renewal.py index d04e2d27c..95f64b94d 100644 --- a/certbot/renewal.py +++ b/certbot/renewal.py @@ -60,7 +60,8 @@ def _reconstitute(config, full_path): try: renewal_candidate = storage.RenewableCert( full_path, configuration.RenewerConfiguration(config)) - except (errors.CertStorageError, IOError): + except (errors.CertStorageError, IOError) as exc: + logger.warning(exc) logger.warning("Renewal configuration file %s is broken. Skipping.", full_path) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None From f8a07a8f4673d0771c6dae034bb755b10ddb8c15 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Mon, 13 Jun 2016 11:53:32 -0700 Subject: [PATCH 112/445] Provide nonroot guidance when logging gets EACCES. --- certbot/main.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index fa14bbf99..f68373998 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -1,6 +1,7 @@ """Certbot main entry point.""" from __future__ import print_function import atexit +import errno import functools import logging.handlers import os @@ -588,8 +589,16 @@ def renew(config, unused_plugins): def setup_log_file_handler(config, logfile, fmt): """Setup file debug logging.""" log_file_path = os.path.join(config.logs_dir, logfile) - handler = logging.handlers.RotatingFileHandler( - log_file_path, maxBytes=2 ** 20, backupCount=10) + try: + handler = logging.handlers.RotatingFileHandler( + log_file_path, maxBytes=2 ** 20, backupCount=10) + except IOError as e: + if e.errno == errno.EACCES: + msg = ("Access denied writing to {0}. To run as non-root, set " + + "--logs-dir, --config-dir, --work-dir to writable paths.") + raise errors.Error(msg.format(log_file_path)) + else: + raise # rotate on each invocation, rollover only possible when maxBytes # is nonzero and backupCount is nonzero, so we set maxBytes as big # as possible not to overrun in single CLI invocation (1MB). From c60c28514c6a8dde2f94c5d557cce7649b67a344 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 13:49:32 -0700 Subject: [PATCH 113/445] Add global DEFAULT variable --- certbot/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index cff111f42..e2475ec31 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -104,6 +104,11 @@ ZERO_ARG_ACTIONS = set(("store_const", "store_true", "store_false", "append_const", "count",)) +# Maps a config option to its default value. This is set during the +# parse_args method of HelpfulArgumentParser. +DEFAULTS = None + + # Maps a config option to a set of config options that may have modified it. # This dictionary is used recursively, so if A modifies B and B modifies C, # it is determined that C was modified by the user if A was modified. From 657c4e725992abba9e2c80a9cd7598d97b79b910 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 14:49:24 -0700 Subject: [PATCH 114/445] Set DEFAULTS during parse_args --- certbot/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index e2475ec31..3c65527e1 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -1,6 +1,7 @@ """Certbot command line argument & config processing.""" from __future__ import print_function import argparse +import copy import glob import logging import logging.handlers @@ -340,6 +341,10 @@ class HelpfulArgumentParser(object): if self.detect_defaults: return parsed_args + global DEFAULTS # pylint: disable=global-statement + DEFAULTS = dict((key, copy.deepcopy(self.parser.get_default(key))) + for key in vars(parsed_args)) + # Do any post-parsing homework here if self.verb == "renew" and not parsed_args.dialog_mode: From b57677b16a994d21659ea2eed7557c9a583712d1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 14:57:14 -0700 Subject: [PATCH 115/445] Use cli.DEFAULTS in storage.py --- certbot/storage.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/certbot/storage.py b/certbot/storage.py index 60886e306..7602ece2d 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -162,30 +162,16 @@ def relevant_values(all_values): from certbot import cli - def _is_cli_default(option, value): - # Look through the CLI parser defaults and see if this option is - # both present and equal to the specified value. If not, return - # False. - # pylint: disable=protected-access - for x in cli.helpful_parser.parser._actions: - if x.dest == option: - if x.default == value: - return True - else: - break - return False - values = dict() for option, value in all_values.iteritems(): # Try to find reasons to store this item in the # renewal config. It can be stored if it is relevant and - # (it is set_by_cli() or flag_default() is different - # from the value or flag_default() doesn't exist). + # (it is set_by_cli(), we don't know the default value, or + # the current value differs from the default value). if _relevant(option): - if (cli.set_by_cli(option) - or not _is_cli_default(option, value)): -# or option not in constants.CLI_DEFAULTS -# or constants.CLI_DEFAULTS[option] != value): + if (cli.set_by_cli(option) or + option not in cli.DEFAULTS or + cli.DEFAULTS[option] != value): values[option] = value return values From 26316fb222cb3cf180810bbf8cdea85e8ddeaf95 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 15:06:49 -0700 Subject: [PATCH 116/445] Ensure changes to webroot_map aren't reflected in cli.DEFAULTS --- certbot/tests/cli_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index f9557abfb..03edaa18f 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -490,6 +490,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods conflicts += ['--staging'] self._check_server_conflict_message(short_args, conflicts) + def test_defaults_global(self): + namespace = self._get_argument_parser()([]) + namespace.webroot_map['example.com'] = '/var/www/html' + + self.assertTrue(cli.DEFAULTS != namespace.webroot_map) + def _certonly_new_request_common(self, mock_client, args=None): with mock.patch('certbot.main._treat_as_renewal') as mock_renewal: mock_renewal.return_value = ("newcert", None) From 8f6866309781f656725c0ef9d6e89a7b291f4aed Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 16:52:35 -0700 Subject: [PATCH 117/445] Add option_was_set function --- certbot/cli.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index 3c65527e1..428227657 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -217,6 +217,21 @@ def set_by_cli(var): set_by_cli.detector = None +def option_was_set(option, value): + """Was option set by the user or does it differ from the default? + + :param str option: configuration variable being considered + :param value: value of the configuration variable named option + + :returns: True if the option was set, otherwise, False + :rtype: bool + + """ + return (set_by_cli(option) or + option not in DEFAULTS or + DEFAULTS[option] != value) + + def argparse_type(variable): "Return our argparse type function for a config variable (default: str)" # pylint: disable=protected-access From aaf93b65b0b1b08bec02e84ea92145d3f15e7081 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 16:55:45 -0700 Subject: [PATCH 118/445] Refactor storage.relevant_values --- certbot/storage.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/certbot/storage.py b/certbot/storage.py index 7602ece2d..82fdbfd54 100644 --- a/certbot/storage.py +++ b/certbot/storage.py @@ -7,8 +7,10 @@ import re import configobj import parsedatetime import pytz +import six import certbot +from certbot import cli from certbot import constants from certbot import crypto_util from certbot import errors @@ -158,22 +160,13 @@ def relevant_values(all_values): :param dict all_values: The original values. :returns: A new dictionary containing items that can be used in renewal. - :rtype dict:""" + :rtype dict: - from certbot import cli - - values = dict() - for option, value in all_values.iteritems(): - # Try to find reasons to store this item in the - # renewal config. It can be stored if it is relevant and - # (it is set_by_cli(), we don't know the default value, or - # the current value differs from the default value). - if _relevant(option): - if (cli.set_by_cli(option) or - option not in cli.DEFAULTS or - cli.DEFAULTS[option] != value): - values[option] = value - return values + """ + return dict( + (option, value) + for option, value in six.iteritems(all_values) + if _relevant(option) and cli.option_was_set(option, value)) class RenewableCert(object): # pylint: disable=too-many-instance-attributes From 8c174f38b5e59ecf2298abf08a84c52110cc4091 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:00:59 -0700 Subject: [PATCH 119/445] Add has_default_value method --- certbot/cli.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 428227657..3c18c3c98 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -217,6 +217,21 @@ def set_by_cli(var): set_by_cli.detector = None +def has_default_value(option, value): + """Does option have the default value? + + If the default value of option is not known, False is returned. + + :param str option: configuration variable being considered + :param value: value of the configuration variable named option + + :returns: True if option has the default value, otherwise, False + :rtype: bool + + """ + return option in DEFAULTS and DEFAULTS[option] == value + + def option_was_set(option, value): """Was option set by the user or does it differ from the default? @@ -227,9 +242,7 @@ def option_was_set(option, value): :rtype: bool """ - return (set_by_cli(option) or - option not in DEFAULTS or - DEFAULTS[option] != value) + return set_by_cli(option) or not has_default_value(option, value) def argparse_type(variable): From 4c68792dd37cc93e17038eba7cd8bee94cdcc6c1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:29:56 -0700 Subject: [PATCH 120/445] Remove test_defaults_global --- certbot/tests/cli_test.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 03edaa18f..f9557abfb 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -490,12 +490,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods conflicts += ['--staging'] self._check_server_conflict_message(short_args, conflicts) - def test_defaults_global(self): - namespace = self._get_argument_parser()([]) - namespace.webroot_map['example.com'] = '/var/www/html' - - self.assertTrue(cli.DEFAULTS != namespace.webroot_map) - def _certonly_new_request_common(self, mock_client, args=None): with mock.patch('certbot.main._treat_as_renewal') as mock_renewal: mock_renewal.return_value = ("newcert", None) From 97af8dfb701edece39ae429852ac1a9784f991e2 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:31:57 -0700 Subject: [PATCH 121/445] Add defaults to helpful_parser --- certbot/cli.py | 14 +++++--------- certbot/tests/storage_test.py | 5 ++--- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 3c18c3c98..ba1f23708 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -105,11 +105,6 @@ ZERO_ARG_ACTIONS = set(("store_const", "store_true", "store_false", "append_const", "count",)) -# Maps a config option to its default value. This is set during the -# parse_args method of HelpfulArgumentParser. -DEFAULTS = None - - # Maps a config option to a set of config options that may have modified it. # This dictionary is used recursively, so if A modifies B and B modifies C, # it is determined that C was modified by the user if A was modified. @@ -229,7 +224,8 @@ def has_default_value(option, value): :rtype: bool """ - return option in DEFAULTS and DEFAULTS[option] == value + return (option in helpful_parser.defaults and + helpful_parser.defaults[option] == value) def option_was_set(option, value): @@ -354,6 +350,7 @@ class HelpfulArgumentParser(object): sys.exit(0) self.visible_topics = self.determine_help_topics(self.help_arg) self.groups = {} # elements are added by .add_group() + self.defaults = {} # elements are added by .parse_args() def parse_args(self): """Parses command line arguments and returns the result. @@ -369,9 +366,8 @@ class HelpfulArgumentParser(object): if self.detect_defaults: return parsed_args - global DEFAULTS # pylint: disable=global-statement - DEFAULTS = dict((key, copy.deepcopy(self.parser.get_default(key))) - for key in vars(parsed_args)) + self.defaults = dict((key, copy.deepcopy(self.parser.get_default(key))) + for key in vars(parsed_args)) # Do any post-parsing homework here diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 0d907eca3..138f6e2fa 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -533,10 +533,9 @@ class RenewableCertTests(BaseRenewableCertTest): """Test that relevant_values() can reject a default value.""" # pylint: disable=protected-access from certbot import storage - mock_parser.verb = "certonly" mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] + mock_parser.defaults = {"rsa_key_size": 2048} + mock_parser.verb = "certonly" self.assertEqual(storage.relevant_values({"rsa_key_size": 2048}), {}) @mock.patch("certbot.cli.helpful_parser") From f9d5ecaf6fc6776988ddf5ea9dbb010ad45eeb7f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 13 Jun 2016 17:45:47 -0700 Subject: [PATCH 122/445] Add option_was_set test --- certbot/tests/cli_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index f9557abfb..9d6335b40 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -447,6 +447,19 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods short_args += '--server example.com'.split() self._check_server_conflict_message(short_args, '--staging') + def test_option_was_set(self): + key_size_option = 'rsa_key_size' + key_size_value = cli.flag_default(key_size_option) + self._get_argument_parser()( + '--rsa-key-size {0}'.format(key_size_value).split()) + + self.assertTrue(cli.option_was_set(key_size_option, key_size_value)) + self.assertTrue(cli.option_was_set('no_verify_ssl', True)) + + config_dir_option = 'config_dir' + self.assertFalse(cli.option_was_set( + config_dir_option, cli.flag_default(config_dir_option))) + def _assert_dry_run_flag_worked(self, namespace, existing_account): self.assertTrue(namespace.dry_run) self.assertTrue(namespace.break_my_certs) From 8c8125c6fd120e7e8d5297ef43ba449b70e924ac Mon Sep 17 00:00:00 2001 From: Geoffroy Doucet Date: Fri, 10 Jun 2016 20:33:25 -0400 Subject: [PATCH 123/445] Added the argument --quiet and -q so then when used with a regular user there is no output to the screen. --- letsencrypt-auto-source/letsencrypt-auto | 13 ++++++++++--- letsencrypt-auto-source/letsencrypt-auto.template | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 569f6d6f3..0ef97c8c1 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -34,6 +34,7 @@ Help for certbot itself cannot be provided until it is installed. -n, --non-interactive, --noninteractive run without asking for user input --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit + -q, --quiet provide only update/error output -v, --verbose provide more output All arguments are accepted and forwarded to the Certbot client when run." @@ -52,15 +53,19 @@ for arg in "$@" ; do HELP=1;; --noninteractive|--non-interactive) ASSUME_YES=1;; + --quiet) + QUIET=1;; --verbose) VERBOSE=1;; -[!-]*) - while getopts ":hnv" short_arg $arg; do + while getopts ":hnvq" short_arg $arg; do case "$short_arg" in h) HELP=1;; n) ASSUME_YES=1;; + q) + QUIET=1;; v) VERBOSE=1;; esac @@ -898,8 +903,10 @@ UNLIKELY_EOF fi if [ -n "$SUDO" ]; then # SUDO is su wrapper or sudo - echo "Requesting root privileges to run certbot..." - echo " $VENV_BIN/letsencrypt" "$@" + if [ "$QUIET" != 1 ]; then + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 73d819b4a..b254cd429 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -34,6 +34,7 @@ Help for certbot itself cannot be provided until it is installed. -n, --non-interactive, --noninteractive run without asking for user input --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit + -q, --quiet provide only update/error output -v, --verbose provide more output All arguments are accepted and forwarded to the Certbot client when run." @@ -52,15 +53,19 @@ for arg in "$@" ; do HELP=1;; --noninteractive|--non-interactive) ASSUME_YES=1;; + --quiet) + QUIET=1;; --verbose) VERBOSE=1;; -[!-]*) - while getopts ":hnv" short_arg $arg; do + while getopts ":hnvq" short_arg $arg; do case "$short_arg" in h) HELP=1;; n) ASSUME_YES=1;; + q) + QUIET=1;; v) VERBOSE=1;; esac @@ -257,8 +262,10 @@ UNLIKELY_EOF fi if [ -n "$SUDO" ]; then # SUDO is su wrapper or sudo - echo "Requesting root privileges to run certbot..." - echo " $VENV_BIN/letsencrypt" "$@" + if [ "$QUIET" != 1 ]; then + echo "Requesting root privileges to run certbot..." + echo " $VENV_BIN/letsencrypt" "$@" + fi fi if [ -z "$SUDO_ENV" ] ; then # SUDO is su wrapper / noop From 61b77766c26233745b922ef8777e6694c49ba053 Mon Sep 17 00:00:00 2001 From: Ben Irving Date: Tue, 14 Jun 2016 11:28:29 -0700 Subject: [PATCH 124/445] Add integration test cases for must staple and ECDSA (#3158) --- tests/boulder-integration.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 323ea004b..ab8fde5f6 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -84,6 +84,24 @@ if [ "$size1" -lt 3000 ] || [ "$size2" -lt 3000 ] || [ "$size3" -gt 1800 ] ; the exit 1 fi +# ECDSA +openssl ecparam -genkey -name secp384r1 -out "${root}/privkey-p384.pem" +SAN="DNS:ecdsa.le.wtf" openssl req -new -sha256 \ + -config "${OPENSSL_CNF:-openssl.cnf}" \ + -key "${root}/privkey-p384.pem" \ + -subj "/" \ + -reqexts san \ + -outform der \ + -out "${root}/csr-p384.der" +common auth --csr "${root}/csr-p384.der" \ + --cert-path "${root}/csr/cert-p384.pem" \ + --chain-path "${root}/csr/chain-p384.pem" +openssl x509 -in "${root}/csr/cert-p384.pem" -text | grep 'ASN1 OID: secp384r1' + +# OCSP Must Staple +common auth --must-staple --domains "must-staple.le.wtf" +openssl x509 -in "${root}/conf/live/must-staple.le.wtf/cert.pem" -text | grep '1.3.6.1.5.5.7.1.24' + # revoke by account key common revoke --cert-path "$root/conf/live/le.wtf/cert.pem" # revoke renewed From 261046a2d7dd01a4ce5383d192ce82d34a92faf8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 14 Jun 2016 13:52:39 -0700 Subject: [PATCH 125/445] Update relevant_values tests --- certbot/tests/storage_test.py | 50 ++++++++++++++++------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 138f6e2fa..261500b98 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -11,6 +11,7 @@ import mock import pytz import certbot +from certbot import cli from certbot import configuration from certbot import errors from certbot.storage import ALL_FOUR @@ -517,38 +518,33 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.exists(temp_config_file)) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values(self, mock_parser): + def _test_relevant_values_common(self, values): + option = "rsa_key_size" + mock_parser = mock.Mock(args=["--standalone"], verb="certonly", + defaults={option: cli.flag_default(option)}) + + from certbot.storage import relevant_values + with mock.patch("certbot.cli.helpful_parser", mock_parser): + return relevant_values(values) + + def test_relevant_values(self): """Test that relevant_values() can reject an irrelevant value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.verb = "certonly" - mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] - self.assertEqual(storage.relevant_values({"hello": "there"}), {}) + self.assertEqual( + self._test_relevant_values_common({"hello": "there"}), {}) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values_default(self, mock_parser): + def test_relevant_values_default(self): """Test that relevant_values() can reject a default value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.args = ["--standalone"] - mock_parser.defaults = {"rsa_key_size": 2048} - mock_parser.verb = "certonly" - self.assertEqual(storage.relevant_values({"rsa_key_size": 2048}), {}) + option = "rsa_key_size" + values = {option: cli.flag_default(option)} + self.assertEqual(self._test_relevant_values_common(values), {}) - @mock.patch("certbot.cli.helpful_parser") - def test_relevant_values_nondefault(self, mock_parser): + def test_relevant_values_nondefault(self): """Test that relevant_values() can retain a non-default value.""" - # pylint: disable=protected-access - from certbot import storage - mock_parser.verb = "certonly" - mock_parser.args = ["--standalone"] - mock_action = mock.Mock(dest="rsa_key_size", default=2048) - mock_parser.parser._actions = [mock_action] - self.assertEqual(storage.relevant_values({"rsa_key_size": 12}), - {"rsa_key_size": 12}) + values = {"rsa_key_size": 12} + # A copy is given to _test_relevant_values_common + # to make sure values isn't modified by the method + self.assertEqual( + self._test_relevant_values_common(values.copy()), values) @mock.patch("certbot.storage.relevant_values") def test_new_lineage(self, mock_rv): From 4158656058cb14e8afa9048b9a3612da73abf1b5 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 14 Jun 2016 16:56:16 -0700 Subject: [PATCH 126/445] Release 0.8.1 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-auto | 52 ++++++++++++++---- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- docs/cli-help.txt | 9 +++ letsencrypt-auto | 52 ++++++++++++++---- letsencrypt-auto-source/certbot-auto.asc | 14 ++--- letsencrypt-auto-source/letsencrypt-auto | 20 +++---- letsencrypt-auto-source/letsencrypt-auto.sig | Bin 256 -> 256 bytes .../pieces/letsencrypt-auto-requirements.txt | 18 +++--- 12 files changed, 120 insertions(+), 55 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index ed133e128..2bcec9128 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.8.1' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index e3dbe4563..87214d2e5 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.8.1' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-auto b/certbot-auto index 2de5ff48f..80f39cf59 100755 --- a/certbot-auto +++ b/certbot-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.0" +LE_AUTO_VERSION="0.8.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -172,7 +172,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi @@ -458,12 +458,39 @@ BootstrapSmartOS() { pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' } +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} + # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon @@ -476,7 +503,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." @@ -500,6 +527,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } @@ -719,15 +747,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.0 \ - --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ - --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da -certbot==0.8.0 \ - --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ - --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 -certbot-apache==0.8.0 \ - --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ - --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index fb56be65f..cd282af50 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.8.1' install_requires = [ 'certbot', diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 62c705b4c..f76cbe596 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.9.0.dev0' +version = '0.8.1' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index 34358a5d9..e5d5629b1 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.9.0.dev0' +__version__ = '0.8.1' diff --git a/docs/cli-help.txt b/docs/cli-help.txt index b25326148..e5f1fdcb4 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -200,6 +200,15 @@ renew: and keys; the shell variable $RENEWED_DOMAINS will contain a space-delimited list of renewed cert domains (default: None) + --disable-hook-validation + Ordinarily the commands specified for --pre-hook + /--post-hook/--renew-hook will be checked for + validity, to see if the programs being run are in the + $PATH, so that mistakes can be caught early, even when + the hooks aren't being run just yet. The validation is + rather simplistic and fails if you use more advanced + shell constructs, so you can use this switch to + disable it. (default: True) certonly: Options for modifying how a cert is obtained diff --git a/letsencrypt-auto b/letsencrypt-auto index 2de5ff48f..80f39cf59 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.0" +LE_AUTO_VERSION="0.8.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -172,7 +172,7 @@ BootstrapDebCommon() { # distro version (#346) virtualenv= - if apt-cache show virtualenv > /dev/null 2>&1; then + if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then virtualenv="virtualenv" fi @@ -458,12 +458,39 @@ BootstrapSmartOS() { pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv' } +BootstrapMageiaCommon() { + if ! $SUDO urpmi --force \ + python \ + libpython-devel \ + python-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi + + if ! $SUDO urpmi --force \ + git \ + gcc \ + cdialog \ + python-augeas \ + libopenssl-devel \ + libffi-devel \ + rootcerts + then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 + fi +} + # Install required OS packages: Bootstrap() { if [ -f /etc/debian_version ]; then echo "Bootstrapping dependencies for Debian-based OSes..." BootstrapDebCommon + elif [ -f /etc/mageia-release ] ; then + # Mageia has both /etc/mageia-release and /etc/redhat-release + ExperimentalBootstrap "Mageia" BootstrapMageiaCommon elif [ -f /etc/redhat-release ]; then echo "Bootstrapping dependencies for RedHat-based OSes..." BootstrapRpmCommon @@ -476,7 +503,7 @@ Bootstrap() { BootstrapArchCommon else echo "Please use pacman to install letsencrypt packages:" - echo "# pacman -S letsencrypt letsencrypt-apache" + echo "# pacman -S certbot certbot-apache" echo echo "If you would like to use the virtualenv way, please run the script again with the" echo "--debug flag." @@ -500,6 +527,7 @@ Bootstrap() { echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." + exit 1 fi } @@ -719,15 +747,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.0 \ - --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ - --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da -certbot==0.8.0 \ - --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ - --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 -certbot-apache==0.8.0 \ - --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ - --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index 0255229b0..5bb725c96 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 -iQEcBAABAgAGBQJXUJvwAAoJEE0XyZXNl3XyvKsH/3qn7Xa/GQx3HvB6Io/Csn/E -v1nbUg5RPwvrTyyol8BJ6UrHiJw+gTbUgCAnBkZ7DYKaC8AQmQXVRcWXNALMMTzB -6LpBXjQQ2xrBYamGj70N7KnTM1QmxI96GUQouiHMJVugV4uihKJDjtR8/f2JWKok -ZSox6E4LqC45HzqLWiOqc13TrHbti32Mo8DyC63PBnSwMnypGLK6XcqM0L9Re62W -smoKu1VWKwWZYRYXIQr0dvK4JmVTrIsdASdZkhTC/vc8y4tGkdN0DcF2EHzci6OA -Tx0W+Ao+HM1ZcaaH3BJ1y3kYfT+mlt6o4OaK3UB/wtUzMmVih7l1UeiNkVL0oYk= -=t3L6 +iQEcBAABAgAGBQJXYJmBAAoJEE0XyZXNl3XyyIMH/jtYFb7rl5XXN8hjlKuK5frq +z7/jdK7fvI+mtYJ4i2Cy3yMz8T4wscXGkhxNtipbATWlpevPfjYzm4ZGC25coFZx +fDX44w0hBBgel7EISXGR1ABXb2rj24TZxIYXwaeClylsK9n5CxcWBocn8tDlfr8t +7VQUJEL3l1IlrnKnvpoL4Eq11sxlIPtitDPJ5c98ZM1293ZbWzIqyZKoXLIUkKHg +pkaa80j/QMmFumxzXFenU91JusLdeoblvjjg+kzjGonjslAYIuH4wEEjz2VJuUYe +P2+2ZyW4eLA6rRZhZ3CMtV79HzTPTWiELCYbXezb+yXJJEqzCYtIXkmbNQ3jUEY= +=86lB -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 1e3118848..80f39cf59 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.9.0.dev0" +LE_AUTO_VERSION="0.8.1" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -747,15 +747,15 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.0 \ - --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ - --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da -certbot==0.8.0 \ - --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ - --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 -certbot-apache==0.8.0 \ - --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ - --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index f024577a34a42779e39755635eca4daf6f46ac2a..e3da0e0a35a6dafe26dbbe061f546aeb929dfcdb 100644 GIT binary patch literal 256 zcmV+b0ssDf@*{!ug8Wu1n(zcS_L8lFE=3`8xR7ru#Yq0;RMek)(d|5ow9N+eJQroy zYC^uLM9I-+sE5&5@?b;V8wXnSYWY@reJu{#UVlBrThxmxIb7 G&VdBMz>LuV literal 256 zcmV+b0ssE80bqRH{`qI)8kxNPxBYvrI}8PM??lS5W%Vrx#G#77ah$EmwK78hzI8fL zpFoy+Z$Ff?%2R#7_m4mldi$VK+OCedjDk7*K$j_4-%~DH(I7GKai+aBwofiSx4Q27 z;SV#~5cO^_zi`sHUA566Xqu@Q&d>p~uV#$A5kiL^Wt5QWl@grCRt$tN>*5-L&1i=@?vr?8a-~$mStKTr+dwJTPqN4;)GA0wY_jCnH7Wesi1I|l7M@y GRi#to9)G0( diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index c33795ced..1291b2c96 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -181,12 +181,12 @@ letsencrypt==0.7.0 \ # THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -acme==0.8.0 \ - --hash=sha256:8561d590e496afb41a8ff2dac389199661d9cd785b1636ae08325771511189af \ - --hash=sha256:dfa86b547628b231f275c7e0efc7a09bec5dfaec866f89f5c5b59b78c14564da -certbot==0.8.0 \ - --hash=sha256:395c5840ff6b75aa51ee6449c86d016c14c5f65a71281e7bcef5feecac6a3293 \ - --hash=sha256:3c3c70b484fb3243a166515adc81ae0401c5d687a2763c75b40df9d8241a4314 -certbot-apache==0.8.0 \ - --hash=sha256:f4d4fc962ecc19646f6745d49c62a265d26e5b2df3acf34ef4865351594156e3 \ - --hash=sha256:cfb211debbcb0d0645c88d7e8bb38c591fca263bfdb5337242c023956055e268 +acme==0.8.1 \ + --hash=sha256:ccd7883772efbf933f91713b8241455993834f3620c8fbd459d9ed5e50bbaaca \ + --hash=sha256:d3ea4acf280bf6253ad7d641cb0970f230a19805acfed809e7a8ddcf62157d9f +certbot==0.8.1 \ + --hash=sha256:89805d9f70249ae859ec4d7a99c00b4bb7083ca90cd12d4d202b76dfc284f7c5 \ + --hash=sha256:6ca8df3d310ced6687d38aac17c0fb8c1b2ec7a3bea156a254e4cc2a1c132771 +certbot-apache==0.8.1 \ + --hash=sha256:c9e3fdc15e65589c2e39eb0e6b1f61f0c0a1db3c17b00bb337f0ff636cc61cb3 \ + --hash=sha256:0faf2879884d3b7a58b071902fba37d4b8b58a50e2c3b8ac262c0a74134045ed From 5c74e728b584e3750ba2b67d057339e8749faecd Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 14 Jun 2016 16:56:31 -0700 Subject: [PATCH 127/445] Bump version to 0.9.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/__init__.py | 2 +- letsencrypt-auto-source/letsencrypt-auto | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 2bcec9128..ed133e128 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.1' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 87214d2e5..e3dbe4563 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.1' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index cd282af50..fb56be65f 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.1' +version = '0.9.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index f76cbe596..62c705b4c 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.8.1' +version = '0.9.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot/__init__.py b/certbot/__init__.py index e5d5629b1..34358a5d9 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.8.1' +__version__ = '0.9.0.dev0' diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 80f39cf59..80b81c898 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" -LE_AUTO_VERSION="0.8.1" +LE_AUTO_VERSION="0.9.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates From 176751f4da939c88741c5438b0ec649e1e9cfbbf Mon Sep 17 00:00:00 2001 From: Jacob Sachs Date: Tue, 14 Jun 2016 23:15:55 -0400 Subject: [PATCH 128/445] Log new cert and cert renewal --- certbot/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot/main.py b/certbot/main.py index f68373998..7d6830b18 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -88,9 +88,11 @@ def _auth_from_domains(le_client, config, domains, lineage=None): hooks.pre_hook(config) try: if action == "renew": + logger.info("Renewing an existing certificate") renewal.renew_cert(config, domains, le_client, lineage) elif action == "newcert": # TREAT AS NEW REQUEST + logger.info("Obtaining a new certificate") lineage = le_client.obtain_and_enroll_certificate(domains) if lineage is False: raise errors.Error("Certificate could not be obtained") From 98a2e0c6d6a519b251f23735d83a40244eab3349 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 15 Jun 2016 16:02:33 -0700 Subject: [PATCH 129/445] Another waypoint --- certbot-nginx/certbot_nginx/nginxparser.py | 20 ++++++++++++-------- certbot-nginx/certbot_nginx/tls_sni_01.py | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 13498bee2..b30a71bea 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -18,8 +18,10 @@ class RawNginxParser(object): left_bracket = Literal("{").suppress() right_bracket = Literal("}").suppress() semicolon = Literal(";").suppress() - space = Optional(White()) key = Word(alphanums + "_/+-.") + space = Optional(White()) + nspace = Optional(Regex(r"[ \t]+")) + blankLine = ZeroOrMore(Regex(r"^\S$")) # Matches anything that is not a special character AND any chars in single # or double quotes value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") @@ -31,23 +33,25 @@ class RawNginxParser(object): comment = space + Literal('#') + restOfLine() assignment = space + key + Optional(space + value, default=None) + semicolon - location_statement = space + Optional(modifier) + Optional(space + location) + location_statement = space + Optional(modifier) + Optional(space + location + space) if_statement = space + Literal("if") + space + Regex(r"\(.+\)") + space map_statement = space + Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space block = Forward() block << Group( # XXX could this "key" be Literal("location")? - (Group(key + location_statement) ^ Group(if_statement) ^ Group(map_statement)) + + # WIP: in order to allow this leaveWhitespace(), we're going to need an explicit + # "whitespaceline" construction... + (Group(space + key + location_statement) ^ Group(if_statement) ^ + Group(map_statement)).leaveWhitespace() + left_bracket + Group(ZeroOrMore(Group(comment | assignment) | block)) + - right_bracket) + space + right_bracket) script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd script.parseWithTabs() - def __init__(self, source): - self.source = source + def __init__(self, source): self.source = source def parse(self): """Returns the parsed tree.""" @@ -74,7 +78,7 @@ class RawNginxDumper(object): indentation = "" if spacey(b[0]): indentation = b.pop(0) - indentation = indentation.replace("\n", "", 1) + #indentation = indentation.replace("\n", "", 1) key = b.pop(0) values = b.pop(0) @@ -103,7 +107,7 @@ class RawNginxDumper(object): def __str__(self): """Return the parsed block as a string.""" print "Merging:\n", '\n'.join(self) - return '\n'.join(self) + '\n' + return ''.join(self) + '\n' diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index efb5d53e6..d24cb830d 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -124,9 +124,9 @@ class NginxTlsSni01(common.TLSSNI01): True, self.challenge_conf) with open(self.challenge_conf, "w") as new_conf: + out = nginxparser.dumps(config) if "mime" in self.challenge_conf: print "Weird" - out = nginxparser.dumps(config) print out #sys.exit(1) #nginxparser.dump(config, new_conf) From 5e59b8ad46872657f9a6fd0a12ae1f1dd8eceaf7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 15 Jun 2016 16:42:47 -0700 Subject: [PATCH 130/445] Woohoo! it works --- certbot-nginx/certbot_nginx/nginxparser.py | 28 +++++++++++++++------- certbot-nginx/certbot_nginx/parser.py | 2 +- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index b30a71bea..c8893f80a 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -15,12 +15,12 @@ class RawNginxParser(object): """A class that parses nginx configuration with pyparsing.""" # constants - left_bracket = Literal("{").suppress() - right_bracket = Literal("}").suppress() - semicolon = Literal(";").suppress() - key = Word(alphanums + "_/+-.") space = Optional(White()) nspace = Optional(Regex(r"[ \t]+")) + left_bracket = Literal("{").suppress() + right_bracket = space.leaveWhitespace() + Literal("}").suppress() + semicolon = Literal(";").suppress() + key = Word(alphanums + "_/+-.") blankLine = ZeroOrMore(Regex(r"^\S$")) # Matches anything that is not a special character AND any chars in single # or double quotes @@ -45,8 +45,8 @@ class RawNginxParser(object): (Group(space + key + location_statement) ^ Group(if_statement) ^ Group(map_statement)).leaveWhitespace() + left_bracket + - Group(ZeroOrMore(Group(comment | assignment) | block)) + - space + right_bracket) + Group(ZeroOrMore(Group(comment | assignment) | block) + space).leaveWhitespace() + + right_bracket) script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd script.parseWithTabs() @@ -76,11 +76,22 @@ class RawNginxDumper(object): b0 = b b = copy.deepcopy(b) indentation = "" + if isinstance(b, str): + yield b + continue if spacey(b[0]): - indentation = b.pop(0) + try: + indentation = b.pop(0) + except: + import ipdb + ipdb.set_trace() + #indentation = indentation.replace("\n", "", 1) key = b.pop(0) values = b.pop(0) + if "worker_processes" in str(b0) and False: + import ipdb + ipdb.set_trace() if isinstance(key, list): yield indentation + "".join(key) + '{' @@ -106,7 +117,8 @@ class RawNginxDumper(object): def __str__(self): """Return the parsed block as a string.""" - print "Merging:\n", '\n'.join(self) + x = ''.join(self) + print "Merging:\n", repr(x) return ''.join(self) + '\n' diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 642702c9f..61f728d2f 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -219,7 +219,7 @@ class NginxParser(object): with open(filename, 'w') as _file: _file.write(out) - if "owncloud" in filename: + if True or "owncloud" in filename: print "Outputting", filename print out a = open("/tmp/nginx/sites-enabled/owncloud.conf").read() From 4f46289c1b9b582f6ad90db0a5651e39d1b2f929 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 15 Jun 2016 17:26:38 -0700 Subject: [PATCH 131/445] Start cleanup --- certbot-nginx/certbot_nginx/nginxparser.py | 44 +++------------------- certbot-nginx/certbot_nginx/parser.py | 17 +-------- certbot-nginx/certbot_nginx/tls_sni_01.py | 5 --- 3 files changed, 7 insertions(+), 59 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index c8893f80a..d1d17493a 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -16,12 +16,10 @@ class RawNginxParser(object): # constants space = Optional(White()) - nspace = Optional(Regex(r"[ \t]+")) left_bracket = Literal("{").suppress() right_bracket = space.leaveWhitespace() + Literal("}").suppress() semicolon = Literal(";").suppress() key = Word(alphanums + "_/+-.") - blankLine = ZeroOrMore(Regex(r"^\S$")) # Matches anything that is not a special character AND any chars in single # or double quotes value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") @@ -40,8 +38,6 @@ class RawNginxParser(object): block << Group( # XXX could this "key" be Literal("location")? - # WIP: in order to allow this leaveWhitespace(), we're going to need an explicit - # "whitespaceline" construction... (Group(space + key + location_statement) ^ Group(if_statement) ^ Group(map_statement)).leaveWhitespace() + left_bracket + @@ -51,7 +47,8 @@ class RawNginxParser(object): script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd script.parseWithTabs() - def __init__(self, source): self.source = source + def __init__(self, source): + self.source = source def parse(self): """Returns the parsed tree.""" @@ -71,27 +68,16 @@ class RawNginxDumper(object): def __iter__(self, blocks=None): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks - print "iterating", blocks for b in blocks: - b0 = b - b = copy.deepcopy(b) - indentation = "" if isinstance(b, str): yield b continue + b = copy.deepcopy(b) + indentation = "" if spacey(b[0]): - try: - indentation = b.pop(0) - except: - import ipdb - ipdb.set_trace() - - #indentation = indentation.replace("\n", "", 1) + indentation = b.pop(0) key = b.pop(0) values = b.pop(0) - if "worker_processes" in str(b0) and False: - import ipdb - ipdb.set_trace() if isinstance(key, list): yield indentation + "".join(key) + '{' @@ -117,14 +103,9 @@ class RawNginxDumper(object): def __str__(self): """Return the parsed block as a string.""" - x = ''.join(self) - print "Merging:\n", repr(x) return ''.join(self) + '\n' - - - # Shortcut functions to respect Python's serialization interface # (like pyyaml, picker or json) @@ -136,16 +117,7 @@ def loads(source): :rtype: list """ - new = UnspacedList(RawNginxParser(source).as_list()) - print "\nNew:" - print new - for entry in new: - print len(entry), " ", - print "\nNewspaced:" - for entry in new.spaced: - print str(len(entry))+ " ", - print "\ngo" - return new + return UnspacedList(RawNginxParser(source).as_list()) def load(_file): @@ -238,7 +210,3 @@ class UnspacedList(list): idx -= 1 pos += 1 return spaces - - def with_spaces(self): - return self.spaced - diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 61f728d2f..18301de0e 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -215,25 +215,10 @@ class NginxParser(object): filename = filename + os.path.extsep + ext try: out = nginxparser.dumps(tree) - logger.debug('Dumping to %s:\n%s', filename, out) + #logger.debug('Writing nginx conf tree to %s:\n%s', filename, out) with open(filename, 'w') as _file: _file.write(out) - if True or "owncloud" in filename: - print "Outputting", filename - print out - a = open("/tmp/nginx/sites-enabled/owncloud.conf").read() - b = open(filename).read() - for linea, lineb in zip(a.split('\n'), b.split('\n')): - if linea != lineb: - print "a", repr(linea) - print "b", repr(lineb) - if a != b: - print "Mismatch!" - if a != out: - print "Double mismatch", len(a), len(out) - else: - print "Match!" except IOError: logger.error("Could not open file for writing: %s", filename) diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index d24cb830d..13ae2c358 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -125,11 +125,6 @@ class NginxTlsSni01(common.TLSSNI01): with open(self.challenge_conf, "w") as new_conf: out = nginxparser.dumps(config) - if "mime" in self.challenge_conf: - print "Weird" - print out - #sys.exit(1) - #nginxparser.dump(config, new_conf) new_conf.write(out) def _make_server_block(self, achall, addrs): From 2cbd680bd543c91f846c98d3bd9972c9d9105b4c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 15 Jun 2016 17:36:53 -0700 Subject: [PATCH 132/445] Hide .spaced from users outside nginxparser.py --- certbot-nginx/certbot_nginx/nginxparser.py | 2 +- certbot-nginx/certbot_nginx/parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index d1d17493a..272e39bbc 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -139,7 +139,7 @@ def dumps(blocks): :rtype: str """ - return str(RawNginxDumper(blocks)) + return str(RawNginxDumper(blocks.spaced)) def dump(blocks, _file): diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 18301de0e..f2faadd58 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -210,7 +210,7 @@ class NginxParser(object): """ for filename in self.parsed: - tree = self.parsed[filename].spaced + tree = self.parsed[filename] if ext: filename = filename + os.path.extsep + ext try: From ff7addefb399ff5ef451a2590755c3976942f101 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 15 Jun 2016 17:57:24 -0700 Subject: [PATCH 133/445] Start fixing tests --- certbot-nginx/certbot_nginx/nginxparser.py | 4 ++-- .../certbot_nginx/tests/nginxparser_test.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 272e39bbc..b2f785d0e 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -22,7 +22,7 @@ class RawNginxParser(object): key = Word(alphanums + "_/+-.") # Matches anything that is not a special character AND any chars in single # or double quotes - value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") + value = Regex(r"((\".*\")?(\'.*\')?[^\{\};, ]?)+") location = CharsNotIn("{};," + string.whitespace) # modifier for location uri [ = | ~ | ~* | ^~ ] modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") @@ -30,7 +30,7 @@ class RawNginxParser(object): # rules comment = space + Literal('#') + restOfLine() - assignment = space + key + Optional(space + value, default=None) + semicolon + assignment = space + key + Optional(space + value, default=None) + space + semicolon location_statement = space + Optional(modifier) + Optional(space + location + space) if_statement = space + Literal("if") + space + Regex(r"\(.+\)") + space map_statement = space + Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 80e82c903..0a09b4a64 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -17,23 +17,23 @@ class TestRawNginxParser(unittest.TestCase): def test_assignments(self): parsed = RawNginxParser.assignment.parseString('root /test;').asList() - self.assertEqual(parsed, ['root', '/test']) + self.assertEqual(parsed, ['root', ' ', '/test']) parsed = RawNginxParser.assignment.parseString('root /test;' 'foo bar;').asList() self.assertEqual(parsed, ['root', '/test'], ['foo', 'bar']) def test_blocks(self): parsed = RawNginxParser.block.parseString('foo {}').asList() - self.assertEqual(parsed, [[['foo'], []]]) + self.assertEqual(parsed, [[['foo', ' '], []]]) parsed = RawNginxParser.block.parseString('location /foo{}').asList() - self.assertEqual(parsed, [[['location', '/foo'], []]]) - parsed = RawNginxParser.block.parseString('foo { bar foo; }').asList() - self.assertEqual(parsed, [[['foo'], [['bar', 'foo']]]]) + self.assertEqual(parsed, [[['location', ' ', '/foo'], []]]) + parsed = RawNginxParser.block.parseString('foo { bar foo ; }').asList() + self.assertEqual(parsed, [[['foo', ' '], [[' ', 'bar', ' ', 'foo', ' '], ' ']]]) def test_nested_blocks(self): parsed = RawNginxParser.block.parseString('foo { bar {} }').asList() block, content = FIRST(parsed) - self.assertEqual(FIRST(content), [['bar'], []]) + self.assertEqual(FIRST(content), [[' ', 'bar', ' '], []]) self.assertEqual(FIRST(block), 'foo') def test_dump_as_string(self): From 5a126a92772a451399b564d708f663def3d324cb Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 16 Jun 2016 12:00:43 -0700 Subject: [PATCH 134/445] ignore bad files in initial sweep --- certbot-apache/certbot_apache/configurator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 9caa4a764..0d6669313 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -511,7 +511,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ addrs = set() - args = self.aug.match(path + "/arg") + try: + args = self.aug.match(path + "/arg") + except RuntimeError: + logger.warn("It looks like one of your paths has a character that your version of augeas can't parse") for arg in args: addrs.add(obj.Addr.fromstring(self.parser.get_arg(arg))) is_ssl = False From e5ce03b312c48b43136628b3fd4a756dc04d754c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 12:24:36 -0700 Subject: [PATCH 135/445] More test wrangling --- certbot-nginx/certbot_nginx/nginxparser.py | 4 ++-- .../certbot_nginx/tests/nginxparser_test.py | 24 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index b2f785d0e..272e39bbc 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -22,7 +22,7 @@ class RawNginxParser(object): key = Word(alphanums + "_/+-.") # Matches anything that is not a special character AND any chars in single # or double quotes - value = Regex(r"((\".*\")?(\'.*\')?[^\{\};, ]?)+") + value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") location = CharsNotIn("{};," + string.whitespace) # modifier for location uri [ = | ~ | ~* | ^~ ] modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~") @@ -30,7 +30,7 @@ class RawNginxParser(object): # rules comment = space + Literal('#') + restOfLine() - assignment = space + key + Optional(space + value, default=None) + space + semicolon + assignment = space + key + Optional(space + value, default=None) + semicolon location_statement = space + Optional(modifier) + Optional(space + location + space) if_statement = space + Literal("if") + space + Regex(r"\(.+\)") + space map_statement = space + Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 0a09b4a64..2a8558bfb 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -5,7 +5,7 @@ import unittest from pyparsing import ParseException from certbot_nginx.nginxparser import ( - RawNginxParser, loads, load, dumps, dump) + RawNginxParser, loads, load, dumps, dump, UnspacedList) from certbot_nginx.tests import util @@ -18,9 +18,8 @@ class TestRawNginxParser(unittest.TestCase): def test_assignments(self): parsed = RawNginxParser.assignment.parseString('root /test;').asList() self.assertEqual(parsed, ['root', ' ', '/test']) - parsed = RawNginxParser.assignment.parseString('root /test;' - 'foo bar;').asList() - self.assertEqual(parsed, ['root', '/test'], ['foo', 'bar']) + parsed = RawNginxParser.assignment.parseString('root /test;foo bar;').asList() + self.assertEqual(parsed, ['root', ' ', '/test'], ['foo', ' ', 'bar']) def test_blocks(self): parsed = RawNginxParser.block.parseString('foo {}').asList() @@ -28,7 +27,7 @@ class TestRawNginxParser(unittest.TestCase): parsed = RawNginxParser.block.parseString('location /foo{}').asList() self.assertEqual(parsed, [[['location', ' ', '/foo'], []]]) parsed = RawNginxParser.block.parseString('foo { bar foo ; }').asList() - self.assertEqual(parsed, [[['foo', ' '], [[' ', 'bar', ' ', 'foo', ' '], ' ']]]) + self.assertEqual(parsed, [[['foo', ' '], [[' ', 'bar', ' ', 'foo '], ' ']]]) def test_nested_blocks(self): parsed = RawNginxParser.block.parseString('foo { bar {} }').asList() @@ -116,7 +115,13 @@ class TestRawNginxParser(unittest.TestCase): def test_dump_as_file(self): with open(util.get_data_filename('nginx.conf')) as handle: - parsed = util.filter_comments(load(handle)) + try: + parsed = load(handle) + except: + handle.seek(0) + print "Failed on", handle.read() + raise + #parsed = util.filter_comments(parsed) parsed[-1][-1].append([['server'], [['listen', '443 ssl'], ['server_name', 'localhost'], @@ -128,12 +133,15 @@ class TestRawNginxParser(unittest.TestCase): [['location', '/'], [['root', 'html'], ['index', 'index.html index.htm']]]]]) + with open(util.get_data_filename('nginx.new.conf'), 'w') as handle: dump(parsed, handle) with open(util.get_data_filename('nginx.new.conf')) as handle: - parsed_new = util.filter_comments(load(handle)) - self.assertEquals(parsed, parsed_new) + parsed_new = load(handle) + self.maxDiff = None + self.assertEquals(parsed[0], parsed_new[0]) + self.assertEquals(parsed[1:], parsed_new[1:]) def test_comments(self): with open(util.get_data_filename('minimalistic_comments.conf')) as handle: From 72ba5b72cc2d11deb85385f9ca3813a4f0aecd2e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 12:25:11 -0700 Subject: [PATCH 136/445] Try to preserve the exact form of end-of-file whitespace --- certbot-nginx/certbot_nginx/nginxparser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 272e39bbc..c8131072c 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -44,7 +44,7 @@ class RawNginxParser(object): Group(ZeroOrMore(Group(comment | assignment) | block) + space).leaveWhitespace() + right_bracket) - script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd + script = OneOrMore(Group(comment | assignment) ^ block) + space + stringEnd script.parseWithTabs() def __init__(self, source): @@ -103,7 +103,7 @@ class RawNginxDumper(object): def __str__(self): """Return the parsed block as a string.""" - return ''.join(self) + '\n' + return ''.join(self) # Shortcut functions to respect Python's serialization interface From 48b03d91cfd6c662866d3d3f061521fae49e46e6 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 16 Jun 2016 12:30:48 -0700 Subject: [PATCH 137/445] return if error --- certbot-apache/certbot_apache/configurator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 0d6669313..647b84bdf 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -515,6 +515,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): args = self.aug.match(path + "/arg") except RuntimeError: logger.warn("It looks like one of your paths has a character that your version of augeas can't parse") + return None for arg in args: addrs.add(obj.Addr.fromstring(self.parser.get_arg(arg))) is_ssl = False @@ -563,7 +564,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): for path in paths: new_vhost = self._create_vhost(path) realpath = os.path.realpath(new_vhost.filep) - if realpath not in vhost_paths.keys(): + if realpath and realpath not in vhost_paths.keys(): vhs.append(new_vhost) vhost_paths[realpath] = new_vhost.filep elif realpath == new_vhost.filep: From 68dd7e9192a40ae7ea32450b2fb696f66ce8757b Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 16 Jun 2016 12:32:34 -0700 Subject: [PATCH 138/445] don't add empty vhosts --- certbot-apache/certbot_apache/configurator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 647b84bdf..57249eea5 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -563,8 +563,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): os.path.basename(path) == "VirtualHost"] for path in paths: new_vhost = self._create_vhost(path) + if not new_vhost: + continue realpath = os.path.realpath(new_vhost.filep) - if realpath and realpath not in vhost_paths.keys(): + if realpath not in vhost_paths.keys(): vhs.append(new_vhost) vhost_paths[realpath] = new_vhost.filep elif realpath == new_vhost.filep: From 50d900718ba0dd3d577edbc67ad4d026e21a7f04 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 16 Jun 2016 16:22:42 -0700 Subject: [PATCH 139/445] add invalid file for cover --- .../apache2/sites-available/old,default.conf | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/old,default.conf diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/old,default.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/old,default.conf new file mode 100644 index 000000000..2bd4e1fe9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/old,default.conf @@ -0,0 +1,12 @@ + + + ServerName ip-172-30-0-17 + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet From b82ebd9180db60333e87ea55318fa9c48490f200 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 17:17:13 -0700 Subject: [PATCH 140/445] Fix desyncronisation with .spaced when modifying sublists - we now actually write directives again! --- certbot-nginx/certbot_nginx/configurator.py | 18 ++++----- certbot-nginx/certbot_nginx/nginxparser.py | 42 +++++++++++++++++---- certbot-nginx/certbot_nginx/parser.py | 8 +++- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 30928e56c..4f46b6a66 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -152,17 +152,17 @@ class NginxConfigurator(common.Plugin): "install a cert.") vhost = self.choose_vhost(domain) - cert_directives = [['ssl_certificate', fullchain_path], - ['ssl_certificate_key', key_path]] + cert_directives = [['\n', 'ssl_certificate', ' ', fullchain_path], + ['\n', 'ssl_certificate_key', ' ', key_path]] # OCSP stapling was introduced in Nginx 1.3.7. If we have that version # or greater, add config settings for it. stapling_directives = [] if self.version >= (1, 3, 7): stapling_directives = [ - ['ssl_trusted_certificate', chain_path], - ['ssl_stapling', 'on'], - ['ssl_stapling_verify', 'on']] + ['\n', 'ssl_trusted_certificate', ' ', chain_path], + ['\n', 'ssl_stapling', ' ', 'on'], + ['\n', 'ssl_stapling_verify', ' ', 'on'], ['\n']] if len(stapling_directives) != 0 and not chain_path: raise errors.PluginError( @@ -337,10 +337,10 @@ class NginxConfigurator(common.Plugin): """ snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() - ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)], - ['ssl_certificate', snakeoil_cert], - ['ssl_certificate_key', snakeoil_key], - ['include', self.parser.loc["ssl_options"]]] + ssl_block = [['\n', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)], + ['\n', 'ssl_certificate', ' ', snakeoil_cert], + ['\n', 'ssl_certificate_key', ' ', snakeoil_key], + ['\n', 'include', ' ', self.parser.loc["ssl_options"]]] self.parser.add_server_directives( vhost.filep, vhost.names, ssl_block, replace=False) vhost.ssl = True diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index c8131072c..856113f71 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -76,6 +76,9 @@ class RawNginxDumper(object): indentation = "" if spacey(b[0]): indentation = b.pop(0) + if not b: + yield indentation + continue key = b.pop(0) values = b.pop(0) @@ -154,36 +157,58 @@ def dump(blocks, _file): return _file.write(dumps(blocks)) -spacey = lambda x: isinstance(x, str) and x.isspace() +spacey = lambda x: (isinstance(x, str) and x.isspace()) or x == '' class UnspacedList(list): """Wrap a list [of lists], making any whitespace entries magically invisible""" - def __init__(self, list_source): + def __init__(self, list_source, top=False): self.spaced = copy.deepcopy(list(list_source)) # Turn self into a version of the source list that has spaces removed # and all sub-lists also UnspacedList()ed list.__init__(self, list_source) + self.top = self for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): - list.__setitem__(self, i, UnspacedList(entry)) + sublist = UnspacedList(entry, top=self.top) + list.__setitem__(self, i, sublist) + assert type(self.spaced) == list, "Type madness %r" % type(self.spaced) + self.spaced[i] = sublist.spaced elif spacey(entry): list.__delitem__(self, i) def insert(self, i, x): - self.spaced.insert(i + self._spaces_before(i), x) + if hasattr(x, "spaced"): + self.spaced.insert(i + self._spaces_before(i), x.spaced) + else: + self.spaced.insert(i + self._spaces_before(i), x) list.insert(self, i, x) def append(self, x): - self.spaced.append(x) + print "Unspaced append", x, self + if hasattr(x, "spaced"): + self.spaced.append(x.spaced) + else: + self.spaced.append(x) list.append(self, x) + print "After: aaaaaaaaaaaaaaaaa" + print self.top + print "Aftertop: bbbbbbbbbbbbbbbbb" + print self.top.spaced + #import ipdb + #ipdb.set_trace() def extend(self, x): - self.spaced.extend(x) + if hasattr(x, "spaced"): + self.spaced.extend(x.spaced) + else: + self.spaced.extend(x) + self.logger.debug("Weird, extending regular list %r to Unspaced %r", x, self) list.extend(self, x) def __add__(self, other): + print "Unspaced add", self, other if hasattr(other, "spaced"): # If the thing added to us is an UnspacedList, use its spaced form self.spaced.__add__(other.spaced) @@ -192,7 +217,10 @@ class UnspacedList(list): list.__add__(self, other) def __setitem__(self, i, value): - self.spaced.__setitem__(i + self._spaces_before(i), value) + if hasattr(value, "spaced"): + self.spaced.__setitem__(i + self._spaces_before(i), value.spaced) + else: + self.spaced.__setitem__(i + self._spaces_before(i), value) list.__setitem__(self, i, value) def __delitem__(self, i): diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index f2faadd58..61c719816 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -215,7 +215,7 @@ class NginxParser(object): filename = filename + os.path.extsep + ext try: out = nginxparser.dumps(tree) - #logger.debug('Writing nginx conf tree to %s:\n%s', filename, out) + logger.debug('Writing nginx conf tree to %s:\n%s', filename, out) with open(filename, 'w') as _file: _file.write(out) @@ -506,6 +506,12 @@ def _add_directive(block, directive, replace): See _add_directives for more documentation. """ + directive = nginxparser.UnspacedList(directive) + print "Unspacified", directive.spaced, directive + if len(directive) == 0: + # whitespace + block.append(directive) + return location = -1 # Find the index of a config line where the name of the directive matches # the name of the directive we want to add. From e76e3a953a81769ebdc1e9890dab6254cc48da96 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 17:58:05 -0700 Subject: [PATCH 141/445] Fix test cases - but we're still mangling files in place... --- certbot-nginx/certbot_nginx/nginxparser.py | 37 ++++++------ .../certbot_nginx/tests/nginxparser_test.py | 57 +++++++++---------- 2 files changed, 43 insertions(+), 51 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 856113f71..d7248ad49 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -1,7 +1,6 @@ """Very low-level nginx config parser based on pyparsing.""" import copy import string -import sys from pyparsing import ( Literal, White, Word, alphanums, CharsNotIn, Forward, Group, @@ -68,11 +67,11 @@ class RawNginxDumper(object): def __iter__(self, blocks=None): """Iterates the dumped nginx content.""" blocks = blocks or self.blocks - for b in blocks: - if isinstance(b, str): - yield b + for b0 in blocks: + if isinstance(b0, str): + yield b0 continue - b = copy.deepcopy(b) + b = copy.deepcopy(b0) indentation = "" if spacey(b[0]): indentation = b.pop(0) @@ -96,13 +95,18 @@ class RawNginxDumper(object): gap = "" # Sometimes the parser has stuck some gap whitespace in here; # if so rotate it into gap - if spacey(values): + if values and spacey(values): gap = values - values = b.pop(0) - if values is None: - yield indentation + key + gap + ';' - else: - yield indentation + key + gap + values + ';' + try: + values = b.pop(0) + except: + import ipdb + ipdb.set_trace() + #if values is None: + # yield indentation + key + gap + ';' + #else: + # yield indentation + key + gap + values + ';' + yield indentation + key + gap + values + ';' def __str__(self): """Return the parsed block as a string.""" @@ -168,12 +172,11 @@ class UnspacedList(list): # Turn self into a version of the source list that has spaces removed # and all sub-lists also UnspacedList()ed list.__init__(self, list_source) - self.top = self + self.top = top if top else self for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): sublist = UnspacedList(entry, top=self.top) list.__setitem__(self, i, sublist) - assert type(self.spaced) == list, "Type madness %r" % type(self.spaced) self.spaced[i] = sublist.spaced elif spacey(entry): list.__delitem__(self, i) @@ -186,18 +189,11 @@ class UnspacedList(list): list.insert(self, i, x) def append(self, x): - print "Unspaced append", x, self if hasattr(x, "spaced"): self.spaced.append(x.spaced) else: self.spaced.append(x) list.append(self, x) - print "After: aaaaaaaaaaaaaaaaa" - print self.top - print "Aftertop: bbbbbbbbbbbbbbbbb" - print self.top.spaced - #import ipdb - #ipdb.set_trace() def extend(self, x): if hasattr(x, "spaced"): @@ -208,7 +204,6 @@ class UnspacedList(list): list.extend(self, x) def __add__(self, other): - print "Unspaced add", self, other if hasattr(other, "spaced"): # If the thing added to us is an UnspacedList, use its spaced form self.spaced.__add__(other.spaced) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 2a8558bfb..2efacd940 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -36,19 +36,20 @@ class TestRawNginxParser(unittest.TestCase): self.assertEqual(FIRST(block), 'foo') def test_dump_as_string(self): - dumped = dumps([ - ['user', 'www-data'], - [['server'], [ - ['listen', '80'], - ['server_name', 'foo.com'], - ['root', '/home/ubuntu/sites/foo/'], - [['location', '/status'], [ - ['check_status', None], - [['types'], [['image/jpeg', 'jpg']]], + dumped = dumps(UnspacedList([ + ['user', ' ', 'www-data'], + [['\n', 'server', ' '], [ + ['\n ', 'listen', ' ', '80'], + ['\n ', 'server_name', ' ', 'foo.com'], + ['\n ', 'root', ' ', '/home/ubuntu/sites/foo/'], + [['\n\n ', 'location', ' ', '/status', ' '], [ + ['\n ', 'check_status', ''], + [['\n\n ', 'types', ' '], + [['\n ', 'image/jpeg', ' ', 'jpg']]], ]] - ]]]) + ]]])) - self.assertEqual(dumped, + self.assertEqual(dumped.split('\n'), 'user www-data;\n' 'server {\n' ' listen 80;\n' @@ -59,10 +60,7 @@ class TestRawNginxParser(unittest.TestCase): ' check_status;\n' '\n' ' types {\n' - ' image/jpeg jpg;\n' - ' }\n' - ' }\n' - '}\n') + ' image/jpeg jpg;}}}'.split('\n')) def test_parse_from_file(self): with open(util.get_data_filename('foo.conf')) as handle: @@ -122,18 +120,17 @@ class TestRawNginxParser(unittest.TestCase): print "Failed on", handle.read() raise #parsed = util.filter_comments(parsed) - parsed[-1][-1].append([['server'], - [['listen', '443 ssl'], - ['server_name', 'localhost'], - ['ssl_certificate', 'cert.pem'], - ['ssl_certificate_key', 'cert.key'], - ['ssl_session_cache', 'shared:SSL:1m'], - ['ssl_session_timeout', '5m'], - ['ssl_ciphers', 'HIGH:!aNULL:!MD5'], - [['location', '/'], - [['root', 'html'], - ['index', 'index.html index.htm']]]]]) - + parsed[-1][-1].append(UnspacedList([['server'], + [['listen', ' ', '443 ssl'], + ['server_name', ' ', 'localhost'], + ['ssl_certificate', ' ', 'cert.pem'], + ['ssl_certificate_key', ' ', 'cert.key'], + ['ssl_session_cache', ' ', 'shared:SSL:1m'], + ['ssl_session_timeout', ' ', '5m'], + ['ssl_ciphers', ' ', 'HIGH:!aNULL:!MD5'], + [['location', ' ', '/'], + [['root', ' ', 'html'], + ['index', ' ', 'index.html index.htm']]]]])) with open(util.get_data_filename('nginx.new.conf'), 'w') as handle: dump(parsed, handle) @@ -159,11 +156,11 @@ class TestRawNginxParser(unittest.TestCase): ['#', " Use bar.conf when it's a full moon!"], ['include', 'foo.conf'], ['#', ' Kilroy was here'], - ['check_status', None], + ['check_status'], [['server'], - [['#', ''], + [['#'], ['#', " Don't forget to open up your firewall!"], - ['#', ''], + ['#'], ['listen', '1234'], ['#', ' listen 80;']]], ]) From da250a4f4bfb5e8feb6efda25745a03cbcd23041 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 17:59:49 -0700 Subject: [PATCH 142/445] Experimentally delete these output files --- .../etc_nginx/minimalistic_comments.new.conf | 11 --- .../tests/testdata/etc_nginx/nginx.new.conf | 83 ------------------- 2 files changed, 94 deletions(-) delete mode 100644 certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/minimalistic_comments.new.conf delete mode 100644 certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/nginx.new.conf diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/minimalistic_comments.new.conf b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/minimalistic_comments.new.conf deleted file mode 100644 index d1b7be91e..000000000 --- a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/minimalistic_comments.new.conf +++ /dev/null @@ -1,11 +0,0 @@ -# Use bar.conf when it's a full moon! -include foo.conf; -# Kilroy was here -check_status; -server { - # - # Don't forget to open up your firewall! - # - listen 1234; - # listen 80; -} diff --git a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/nginx.new.conf b/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/nginx.new.conf deleted file mode 100644 index 59c1c968f..000000000 --- a/certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/nginx.new.conf +++ /dev/null @@ -1,83 +0,0 @@ -user nobody; -worker_processes 1; -error_log logs/error.log; -error_log logs/error.log notice; -error_log logs/error.log info; -pid logs/nginx.pid; -events { - worker_connections 1024; -} -include foo.conf; -http { - include mime.types; - include sites-enabled/*; - default_type application/octet-stream; - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - access_log logs/access.log main; - sendfile on; - tcp_nopush on; - keepalive_timeout 0; - gzip on; - - server { - listen 8080; - server_name localhost; - server_name ~^(www\.)?(example|bar)\.; - charset koi8-r; - access_log logs/host.access.log main; - - location / { - root html; - index index.html index.htm; - } - error_page 404 /404.html; - error_page 500 502 503 504 /50x.html; - - location = /50x.html { - root html; - } - - location ~ \.php$ { - proxy_pass http://127.0.0.1; - } - - location ~ \.php$ { - root html; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; - } - - location ~ /\.ht { - deny all; - } - } - - server { - listen 8000; - listen somename:8080; - include server.conf; - - location / { - root html; - index index.html index.htm; - } - } - - server { - listen 443 ssl; - server_name localhost; - ssl_certificate cert.pem; - ssl_certificate_key cert.key; - ssl_session_cache shared:SSL:1m; - ssl_session_timeout 5m; - ssl_ciphers HIGH:!aNULL:!MD5; - - location / { - root html; - index index.html index.htm; - } - } -} From 9459daa6d03940f6c8fdfe169807a32df2a36b14 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 18:04:13 -0700 Subject: [PATCH 143/445] Delete new.conf files after tests Not least, to prevent git conflicts with old branches --- .../certbot_nginx/tests/nginxparser_test.py | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 2efacd940..87c7a0430 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -136,9 +136,12 @@ class TestRawNginxParser(unittest.TestCase): dump(parsed, handle) with open(util.get_data_filename('nginx.new.conf')) as handle: parsed_new = load(handle) - self.maxDiff = None - self.assertEquals(parsed[0], parsed_new[0]) - self.assertEquals(parsed[1:], parsed_new[1:]) + try: + self.maxDiff = None + self.assertEquals(parsed[0], parsed_new[0]) + self.assertEquals(parsed[1:], parsed_new[1:]) + finally: + os.unlink(util.get_data_filename('nginx.new.conf')) def test_comments(self): with open(util.get_data_filename('minimalistic_comments.conf')) as handle: @@ -150,20 +153,23 @@ class TestRawNginxParser(unittest.TestCase): with open(util.get_data_filename('minimalistic_comments.new.conf')) as handle: parsed_new = load(handle) - self.assertEquals(parsed, parsed_new) + try: + self.assertEquals(parsed, parsed_new) - self.assertEqual(parsed_new, [ - ['#', " Use bar.conf when it's a full moon!"], - ['include', 'foo.conf'], - ['#', ' Kilroy was here'], - ['check_status'], - [['server'], - [['#'], - ['#', " Don't forget to open up your firewall!"], - ['#'], - ['listen', '1234'], - ['#', ' listen 80;']]], - ]) + self.assertEqual(parsed_new, [ + ['#', " Use bar.conf when it's a full moon!"], + ['include', 'foo.conf'], + ['#', ' Kilroy was here'], + ['check_status'], + [['server'], + [['#'], + ['#', " Don't forget to open up your firewall!"], + ['#'], + ['listen', '1234'], + ['#', ' listen 80;']]], + ]) + finally: + os.unlink(util.get_data_filename('minimalistic_comments.new.conf')) def test_issue_518(self): parsed = loads('if ($http_accept ~* "webp") { set $webp "true"; }') From efd1ff46c696294341f9f8537e25023c8d01f196 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 16 Jun 2016 18:18:33 -0700 Subject: [PATCH 144/445] Lint --- certbot-nginx/certbot_nginx/nginxparser.py | 10 ++++------ certbot-nginx/certbot_nginx/parser.py | 1 - certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 1 + certbot-nginx/certbot_nginx/tls_sni_01.py | 4 +--- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index d7248ad49..1664f94cf 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -1,5 +1,6 @@ """Very low-level nginx config parser based on pyparsing.""" import copy +import logging import string from pyparsing import ( @@ -8,6 +9,7 @@ from pyparsing import ( from pyparsing import stringEnd from pyparsing import restOfLine +logger = logging.getLogger(__name__) class RawNginxParser(object): # pylint: disable=expression-not-assigned @@ -97,11 +99,7 @@ class RawNginxDumper(object): # if so rotate it into gap if values and spacey(values): gap = values - try: - values = b.pop(0) - except: - import ipdb - ipdb.set_trace() + values = b.pop(0) #if values is None: # yield indentation + key + gap + ';' #else: @@ -200,7 +198,7 @@ class UnspacedList(list): self.spaced.extend(x.spaced) else: self.spaced.extend(x) - self.logger.debug("Weird, extending regular list %r to Unspaced %r", x, self) + logger.debug("Weird, extending regular list %r to Unspaced %r", x, self) list.extend(self, x) def __add__(self, other): diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 61c719816..21127bc08 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -4,7 +4,6 @@ import logging import os import pyparsing import re -import sys from certbot import errors diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 87c7a0430..7354c118c 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -1,5 +1,6 @@ """Test for certbot_nginx.nginxparser.""" import operator +import os import unittest from pyparsing import ParseException diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index 13ae2c358..e4c5d31a6 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -3,7 +3,6 @@ import itertools import logging import os -import sys from certbot import errors from certbot.plugins import common @@ -124,8 +123,7 @@ class NginxTlsSni01(common.TLSSNI01): True, self.challenge_conf) with open(self.challenge_conf, "w") as new_conf: - out = nginxparser.dumps(config) - new_conf.write(out) + nginxparser.dump(config, new_conf) def _make_server_block(self, achall, addrs): """Creates a server block for a challenge. From 6666ff25d49deb74c7976dd12a9a384ddde39083 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 17 Jun 2016 03:57:49 +0200 Subject: [PATCH 145/445] Moving import dialog to the top of the file --- certbot/tests/cli_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 9a2009e43..9c81c070b 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -2,6 +2,7 @@ from __future__ import print_function import argparse +import dialog import functools import itertools import os @@ -923,8 +924,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods traceback.format_exception_only(KeyboardInterrupt, interrupt))) # Test dialog errors - - import dialog exception = dialog.error(message="test message") main._handle_exception( dialog.DialogError, exc_value=exception, trace=None, config=None) From 31d9fc7d865c4567f56b3991c90e915a620aaa2e Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Fri, 17 Jun 2016 04:13:44 +0200 Subject: [PATCH 146/445] Adding --skip-missing-interpreters to constributing.rst "Submitting a pull request" section --- docs/contributing.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 9ceb0fbdd..633af19c6 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -317,7 +317,9 @@ Steps: 3. Run ``./pep8.travis.sh`` to do a cursory check of your code style. Fix any errors. 4. Run ``tox -e lint`` to check for pylint errors. Fix any errors. -5. Run ``tox`` to run the entire test suite including coverage. Fix any errors. +5. Run ``tox --skip-missing-interpreters`` to run the entire test suite + including coverage. The ``--skip-missing-interpreters`` argument ignores + missing versions of Python needed for running the tests. Fix any errors. 6. If your code touches communication with an ACME server/Boulder, you should run the integration tests, see `integration`_. See `Known Issues`_ for some common failures that have nothing to do with your code. From f5282ed2de63342d09e593cb1a9338383ab2b9b1 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 17 Jun 2016 10:48:54 -0700 Subject: [PATCH 147/445] Show lines missing test coverage in test output --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..087900105 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[report] +# show lines missing coverage in output +show_missing = True From 340e4dc0f27f5a2db86e2e639f5392723bf1204e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 17 Jun 2016 10:50:02 -0700 Subject: [PATCH 148/445] Remove unused nosexcover dependency --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 53fde5282..21cda901a 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,6 @@ dev_extras = [ 'astroid==1.3.5', 'coverage', 'nose', - 'nosexcover', 'pep8', 'pylint==1.4.2', # upstream #248 'tox', From ba0a0e9c2664ffade3bc916cf5fc10d8c5d57a53 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 17 Jun 2016 14:32:24 -0700 Subject: [PATCH 149/445] Tests for UnspacedList --- certbot-nginx/certbot_nginx/nginxparser.py | 20 +++---- .../certbot_nginx/tests/nginxparser_test.py | 54 +++++++++++++++++++ 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 1664f94cf..caf6fe725 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -164,16 +164,15 @@ spacey = lambda x: (isinstance(x, str) and x.isspace()) or x == '' class UnspacedList(list): """Wrap a list [of lists], making any whitespace entries magically invisible""" - def __init__(self, list_source, top=False): + def __init__(self, list_source): self.spaced = copy.deepcopy(list(list_source)) # Turn self into a version of the source list that has spaces removed # and all sub-lists also UnspacedList()ed list.__init__(self, list_source) - self.top = top if top else self for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): - sublist = UnspacedList(entry, top=self.top) + sublist = UnspacedList(entry) list.__setitem__(self, i, sublist) self.spaced[i] = sublist.spaced elif spacey(entry): @@ -202,12 +201,9 @@ class UnspacedList(list): list.extend(self, x) def __add__(self, other): - if hasattr(other, "spaced"): - # If the thing added to us is an UnspacedList, use its spaced form - self.spaced.__add__(other.spaced) - else: - self.spaced.__add__(other) - list.__add__(self, other) + l = copy.deepcopy(self) + l.extend(other) + return l def __setitem__(self, i, value): if hasattr(value, "spaced"): @@ -220,6 +216,12 @@ class UnspacedList(list): self.spaced.__delitem__(i + self._spaces_before(i)) list.__delitem__(self, i) + def __deepcopy__(self, memo): + l = UnspacedList(self[:]) + l.spaced = copy.deepcopy(self.spaced) + return l + + def _spaces_before(self, idx): "Count the number of spaces in the spaced list before pos idx in the spaceless one" spaces = 0 diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 7354c118c..e78adfac1 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -1,4 +1,5 @@ """Test for certbot_nginx.nginxparser.""" +import copy import operator import os import unittest @@ -180,5 +181,58 @@ class TestRawNginxParser(unittest.TestCase): [['set', '$webp "true"']]] ]) +class TestUnspacedList(unittest.TestCase): + """Test the raw low-level Nginx config parser.""" + def setUp(self): + self.a = ["\n ", "things", " ", "quirk"] + self.b = ["y", " "] + self.l = self.a[:] + self.l2 = self.b[:] + self.ul = UnspacedList(self.l) + self.ul2 = UnspacedList(self.l2) + + def test_construction(self): + self.assertEqual(self.ul, ["things", "quirk"]) + self.assertEqual(self.ul2, ["y"]) + + def test_append(self): + ul3 = copy.deepcopy(self.ul) + ul3.append("wise") + self.assertEqual(ul3, ["things", "quirk", "wise"]) + self.assertEqual(ul3.spaced, self.a + ["wise"]) + + def test_add(self): + ul3 = self.ul + self.ul2 + self.assertEqual(ul3, ["things", "quirk", "y"]) + self.assertEqual(ul3.spaced, self.a + self.b) + self.assertEqual(self.ul.spaced, self.a) + ul3 = self.ul + self.l2 + self.assertEqual(ul3, ["things", "quirk", "y", " "]) + self.assertEqual(ul3.spaced, self.a + self.b) + + def test_extend(self): + ul3 = copy.deepcopy(self.ul) + ul3.extend(self.ul2) + self.assertEqual(ul3, ["things", "quirk", "y"]) + self.assertEqual(ul3.spaced, self.a + self.b) + self.assertEqual(self.ul.spaced, self.a) + + def test_set(self): + ul3 = copy.deepcopy(self.ul) + ul3[0] = "zither" + l = ["\n ", "zather", "zest"] + ul3[1] = UnspacedList(l) + self.assertEqual(ul3, ["zither", ["zather", "zest"]]) + self.assertEqual(ul3.spaced, [self.a[0], "zither", " ", l]) + + def test_rawlists(self): + ul3 = copy.deepcopy(self.ul) + ul3.insert(0, "some") + ul3.append("why") + ul3.extend(["did", "whether"]) + del ul3[2] + self.assertEqual(ul3, ["some", "things", "why", "did", "whether"]) + + if __name__ == '__main__': unittest.main() # pragma: no cover From b2c36f85274176a31ed16bfe7b210793165d3fac Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 17 Jun 2016 14:39:55 -0700 Subject: [PATCH 150/445] Lint & test fix --- certbot-nginx/certbot_nginx/nginxparser.py | 2 +- certbot-nginx/certbot_nginx/tests/parser_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index caf6fe725..765194806 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -216,7 +216,7 @@ class UnspacedList(list): self.spaced.__delitem__(i + self._spaces_before(i)) list.__delitem__(self, i) - def __deepcopy__(self, memo): + def __deepcopy__(self, unused_memo): l = UnspacedList(self[:]) l.spaced = copy.deepcopy(self.spaced) return l diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index 8ac995dfc..6d046178a 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -126,7 +126,7 @@ class NginxParserTest(util.NginxTest): nparser.add_server_directives(nparser.abs_path('nginx.conf'), set(['localhost', r'~^(www\.)?(example|bar)\.']), - [['foo', 'bar'], ['ssl_certificate', + [['foo', 'bar'], ['\n ', 'ssl_certificate', ' ', '/etc/ssl/cert.pem']], replace=False) ssl_re = re.compile(r'\n\s+ssl_certificate /etc/ssl/cert.pem') From 6156b452bcf7c924f12c334e7094ff5a9049ee37 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 24 May 2016 14:04:55 -0500 Subject: [PATCH 151/445] Fix FQDN checks. --- certbot/util.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/certbot/util.py b/certbot/util.py index 35c599737..9739e8d2f 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -423,14 +423,17 @@ def enforce_domain_sanity(domain): # It wasn't an IP address, so that's good pass - # FQDN checks from - # http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/ - # Characters used, domain parts < 63 chars, tld > 1 < 64 chars - # first and last char is not "-" - fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? 256: + raise errors.ConfigurationError(msg + "it is too long.") + return domain From a7a2049d69debf457cec637ddf4e5c9ca9848c9f Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 24 May 2016 14:14:44 -0500 Subject: [PATCH 152/445] Fix FQDN tests. --- certbot/tests/cli_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 9c81c070b..896550837 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -342,11 +342,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods # FQDN self.assertRaises(errors.ConfigurationError, self._call, - ['-d', 'comma,gotwrong.tld']) + ['-d', 'a' * 64]) # FQDN 2 self.assertRaises(errors.ConfigurationError, self._call, - ['-d', 'illegal.character=.tld']) + ['-d', (('a' * 50) + '.') * 10]) # Wildcard self.assertRaises(errors.ConfigurationError, self._call, From 2625daad75a9233ee0f2d57576adff23e6db1779 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 24 May 2016 14:25:29 -0500 Subject: [PATCH 153/445] Fix more FQDN tests in ops_test.py --- certbot/tests/display/ops_test.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 3aff37d86..26f67b69f 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -248,9 +248,9 @@ class ChooseNamesTest(unittest.TestCase): def test_get_valid_domains(self): from certbot.display.ops import get_valid_domains all_valid = ["example.com", "second.example.com", - "also.example.com"] - all_invalid = ["xn--ls8h.tld", "*.wildcard.com", "notFQDN", - "uniçodé.com"] + "also.example.com", "under_score.example.com", + "justtld"] + all_invalid = ["xn--ls8h.tld", "*.wildcard.com", "uniçodé.com"] two_valid = ["example.com", "xn--ls8h.tld", "also.example.com"] self.assertEqual(get_valid_domains(all_valid), all_valid) self.assertEqual(get_valid_domains(all_invalid), []) @@ -276,19 +276,18 @@ class ChooseNamesTest(unittest.TestCase): mock_util().input.return_value = (display_util.OK, "xn--ls8h.tld") self.assertEqual(_choose_names_manually(), []) - # non-FQDN and no retry - mock_util().input.return_value = (display_util.OK, - "notFQDN") - self.assertEqual(_choose_names_manually(), []) - # Two valid domains + # Valid domains mock_util().input.return_value = (display_util.OK, ("example.com," + "under_score.example.com," + "justtld," "valid.example.com")) self.assertEqual(_choose_names_manually(), - ["example.com", "valid.example.com"]) + ["example.com", "under_score.example.com", + "justtld", "valid.example.com"]) # Three iterations mock_util().input.return_value = (display_util.OK, - "notFQDN") + "uniçodé.com") yn = mock.MagicMock() yn.side_effect = [True, True, False] mock_util().yesno = yn From a148d2ddfa60206f1a648dd7c9851e5921ea1805 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Fri, 17 Jun 2016 18:58:48 -0500 Subject: [PATCH 154/445] Limit domains to 255 octets. --- certbot/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/util.py b/certbot/util.py index 9739e8d2f..301fc669b 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -431,7 +431,7 @@ def enforce_domain_sanity(domain): for l in labels: if not 0 < len(l) < 64: raise errors.ConfigurationError(msg + "label {0} is too long.".format(l)) - if len(domain) > 256: + if len(domain) > 255: raise errors.ConfigurationError(msg + "it is too long.") return domain From 679101cfb0659eba484a625bc244a595d86edf93 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 17 Jun 2016 18:29:45 -0700 Subject: [PATCH 155/445] Object printing improvements --- certbot-nginx/certbot_nginx/obj.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 0d1151f39..20f9f0a6f 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -85,6 +85,9 @@ class Addr(common.Addr): return parts + def __repr__(self): + return "Addr(" + self.__str__() + ")" + def __eq__(self, other): if isinstance(other, self.__class__): return (self.tup == other.tup and @@ -126,6 +129,9 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods "enabled: %s" % (self.filep, addr_str, self.names, self.ssl, self.enabled)) + def __repr__(self): + return "VirtualHost(" + self.__str__().replace("\n",",") + ")\n" + def __eq__(self, other): if isinstance(other, self.__class__): return (self.filep == other.filep and From 7bcc23d9f54fea226538dc3d6d4e7f2d37232977 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 17 Jun 2016 18:30:12 -0700 Subject: [PATCH 156/445] Debugging --- certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 3264d6ed3..19d22365d 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -12,6 +12,7 @@ from certbot import errors from certbot.plugins import common_test from certbot.tests import acme_util +from certbot_nginx import nginxparser from certbot_nginx import obj from certbot_nginx.tests import util @@ -132,16 +133,21 @@ class TlsSniPerformTest(util.NginxTest): http = self.sni.configurator.parser.parsed[ self.sni.configurator.parser.loc["root"]][-1] + print "http", http + #print "SPACED\n", http.spaced self.assertTrue(['include', self.sni.challenge_conf] in http[1]) vhosts = self.sni.configurator.parser.get_vhosts() + print "Got", vhosts vhs = [vh for vh in vhosts if vh.filep == self.sni.challenge_conf] + print "And now", vhs for vhost in vhs: if vhost.addrs == set(v_addr1): response = self.achalls[0].response(self.account_key) else: response = self.achalls[2].response(self.account_key) + print vhost.addrs, set(v_addr2) self.assertEqual(vhost.addrs, set(v_addr2)) self.assertEqual(vhost.names, set([response.z_domain])) From 3c9f4d5fc73204ce9aa840e4eabd8b3dbd01cf74 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sat, 18 Jun 2016 23:50:30 +0300 Subject: [PATCH 157/445] If port is set for any IP, do not attempt to autoconfigure --- certbot-apache/certbot_apache/configurator.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index e4c06ba7e..392670985 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -624,11 +624,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Note: This could be made to also look for ip:443 combo listens = [self.parser.get_arg(x).split()[0] for x in self.parser.find_dir("Listen")] + # In case no Listens are set (which really is a broken apache config) if not listens: listens = ["80"] - if port in listens: + + # Listen already in place + if self._has_port_already(listens, port): return + for listen in listens: # For any listen statement, check if the machine also listens on # Port 443. If not, add such a listen statement. @@ -664,6 +668,23 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.parser.loc["listen"]) listens.append("%s:%s" % (ip, port)) + def _has_port_already(self, listens, port): + """Helper method for prepare_server_https to find out if user + already has an active Listen statement for the port we need + + :param list listens: List of listen variables + :param string port: Port in question + """ + + if port in listens: + return True + # Check if Apache is already listening on a specific IP + for listen in listens: + if len(listen.split(":")) > 1: + # Ugly but takes care of protocol def, eg: 1.1.1.1:443 https + if listen.split(":")[-1].split(" ")[0] == port: + return True + def prepare_https_modules(self, temp): """Helper method for prepare_server_https, taking care of enabling needed modules From e4f88506cc84b852b6bc339cede9ed8abfebc2db Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 18 Jun 2016 14:52:07 -0700 Subject: [PATCH 158/445] Fix TLS_SNI & associated tests --- certbot-nginx/certbot_nginx/nginxparser.py | 20 +++++++++++++----- certbot-nginx/certbot_nginx/obj.py | 2 +- certbot-nginx/certbot_nginx/parser.py | 7 +++++-- .../certbot_nginx/tests/configurator_test.py | 2 +- .../certbot_nginx/tests/nginxparser_test.py | 4 ++-- .../certbot_nginx/tests/tls_sni_01_test.py | 5 ----- certbot-nginx/certbot_nginx/tests/util.py | 11 +++++++--- certbot-nginx/certbot_nginx/tls_sni_01.py | 21 ++++++++++--------- 8 files changed, 43 insertions(+), 29 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 765194806..6b3896fd9 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -165,6 +165,7 @@ class UnspacedList(list): """Wrap a list [of lists], making any whitespace entries magically invisible""" def __init__(self, list_source): + # ensure our argument is not a generator, and duplicate any sublists self.spaced = copy.deepcopy(list(list_source)) # Turn self into a version of the source list that has spaces removed @@ -173,16 +174,25 @@ class UnspacedList(list): for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): sublist = UnspacedList(entry) - list.__setitem__(self, i, sublist) + if sublist != [] or sublist.spaced == []: + list.__setitem__(self, i, sublist) + else: + # if a sublist is exclusively spacey entries, it might + # choke the high level parser, so make it disappear + list.__delitem__(self, i) self.spaced[i] = sublist.spaced elif spacey(entry): - list.__delitem__(self, i) + # don't delete comments + if "#" not in self[:i]: + list.__delitem__(self, i) def insert(self, i, x): - if hasattr(x, "spaced"): - self.spaced.insert(i + self._spaces_before(i), x.spaced) - else: + if not isinstance(x, list): # str or None self.spaced.insert(i + self._spaces_before(i), x) + else: + if not hasattr(x, "spaced"): + x = UnspacedList(x) + self.spaced.insert(i + self._spaces_before(i), x.spaced) list.insert(self, i, x) def append(self, x): diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 20f9f0a6f..a559b5e02 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -130,7 +130,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods self.names, self.ssl, self.enabled)) def __repr__(self): - return "VirtualHost(" + self.__str__().replace("\n",",") + ")\n" + return "VirtualHost(" + self.__str__().replace("\n",", ") + ")\n" def __eq__(self, other): if isinstance(other, self.__class__): diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 21127bc08..31595d56d 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -1,4 +1,5 @@ """NginxParser is a member object of the NginxConfigurator class.""" +import copy import glob import logging import os @@ -113,6 +114,7 @@ class NginxParser(object): for filename in servers: for server in servers[filename]: # Parse the server block into a VirtualHost object + parsed_server = parse_server(server) vhost = obj.VirtualHost(filename, parsed_server['addrs'], @@ -132,7 +134,7 @@ class NginxParser(object): :rtype: list """ - result = list(block) # Copy the list to keep self.parsed idempotent + result = copy.deepcopy(block) # Copy the list to keep self.parsed idempotent for directive in block: if _is_include_directive(directive): included_files = glob.glob( @@ -465,6 +467,8 @@ def parse_server(server): 'names': set()} for directive in server: + if not directive: + continue if directive[0] == 'listen': addr = obj.Addr.fromstring(directive[1]) parsed_server['addrs'].add(addr) @@ -506,7 +510,6 @@ def _add_directive(block, directive, replace): """ directive = nginxparser.UnspacedList(directive) - print "Unspacified", directive.spaced, directive if len(directive) == 0: # whitespace block.append(directive) diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index 30f287249..a1d0e75cc 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -83,7 +83,7 @@ class NginxConfiguratorTest(util.NginxTest): filep = self.config.parser.abs_path('sites-enabled/example.com') self.config.parser.add_server_directives( filep, set(['.example.com', 'example.*']), - [['listen', '5001 ssl']], + [['listen', ' ', '5001 ssl']], replace=False) self.config.save() diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index e78adfac1..2e8011443 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -164,9 +164,9 @@ class TestRawNginxParser(unittest.TestCase): ['#', ' Kilroy was here'], ['check_status'], [['server'], - [['#'], + [['#', ''], ['#', " Don't forget to open up your firewall!"], - ['#'], + ['#', ''], ['listen', '1234'], ['#', ' listen 80;']]], ]) diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 19d22365d..51301145c 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -133,21 +133,16 @@ class TlsSniPerformTest(util.NginxTest): http = self.sni.configurator.parser.parsed[ self.sni.configurator.parser.loc["root"]][-1] - print "http", http - #print "SPACED\n", http.spaced self.assertTrue(['include', self.sni.challenge_conf] in http[1]) vhosts = self.sni.configurator.parser.get_vhosts() - print "Got", vhosts vhs = [vh for vh in vhosts if vh.filep == self.sni.challenge_conf] - print "And now", vhs for vhost in vhs: if vhost.addrs == set(v_addr1): response = self.achalls[0].response(self.account_key) else: response = self.achalls[2].response(self.account_key) - print vhost.addrs, set(v_addr2) self.assertEqual(vhost.addrs, set(v_addr2)) self.assertEqual(vhost.names, set([response.z_domain])) diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index ddacd041b..4ea4a03a7 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -1,4 +1,5 @@ """Common utilities for certbot_nginx.""" +import copy import os import pkg_resources import unittest @@ -16,6 +17,7 @@ from certbot.plugins import common from certbot_nginx import constants from certbot_nginx import configurator +from certbot_nginx import nginxparser class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods @@ -82,12 +84,15 @@ def filter_comments(tree): def traverse(tree): """Generator dropping comment nodes""" - for key, values in tree: + for entry in tree: + key, values = entry if isinstance(key, list): - yield [key, filter_comments(values)] + new = copy.deepcopy(entry) + new[1] = filter_comments(values) + yield new else: if key != '#': - yield [key, values] + yield entry return list(traverse(tree)) diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index e4c5d31a6..a9f31b84a 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -93,10 +93,10 @@ class NginxTlsSni01(common.TLSSNI01): # Add the 'include' statement for the challenges if it doesn't exist # already in the main config included = False - include_directive = ['include', self.challenge_conf] + include_directive = ['include', ' ', self.challenge_conf] root = self.configurator.parser.loc["root"] - bucket_directive = ['server_names_hash_bucket_size', '128'] + bucket_directive = ['server_names_hash_bucket_size', ' ', '128'] main = self.configurator.parser.parsed[root] for key, body in main: @@ -118,6 +118,7 @@ class NginxTlsSni01(common.TLSSNI01): config = [self._make_server_block(pair[0], pair[1]) for pair in itertools.izip(self.achalls, ll_addrs)] + config = nginxparser.UnspacedList(config) self.configurator.reverter.register_file_creation( True, self.challenge_conf) @@ -142,19 +143,19 @@ class NginxTlsSni01(common.TLSSNI01): document_root = os.path.join( self.configurator.config.work_dir, "tls_sni_01_page") - block = [['listen', str(addr)] for addr in addrs] + block = [['listen', ' ', str(addr)] for addr in addrs] - block.extend([['server_name', + block.extend([['server_name', ' ', achall.response(achall.account_key).z_domain], - ['include', self.configurator.parser.loc["ssl_options"]], + ['include', ' ', self.configurator.parser.loc["ssl_options"]], # access and error logs necessary for # integration testing (non-root) - ['access_log', os.path.join( + ['access_log', ' ', os.path.join( self.configurator.config.work_dir, 'access.log')], - ['error_log', os.path.join( + ['error_log', ' ', os.path.join( self.configurator.config.work_dir, 'error.log')], - ['ssl_certificate', self.get_cert_path(achall)], - ['ssl_certificate_key', self.get_key_path(achall)], - [['location', '/'], [['root', document_root]]]]) + ['ssl_certificate', ' ', self.get_cert_path(achall)], + ['ssl_certificate_key', ' ', self.get_key_path(achall)], + [['location', ' ', '/'], [['root', ' ', document_root]]]]) return [['server'], block] From 1e59bf81123df94210f5a4627e1af7e444b1eff2 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 18 Jun 2016 14:53:17 -0700 Subject: [PATCH 159/445] fixup --- certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 51301145c..3264d6ed3 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -12,7 +12,6 @@ from certbot import errors from certbot.plugins import common_test from certbot.tests import acme_util -from certbot_nginx import nginxparser from certbot_nginx import obj from certbot_nginx.tests import util From b663ec039ed41012f89d2dcac950be323a7d3a19 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 18 Jun 2016 15:05:48 -0700 Subject: [PATCH 160/445] lint --- certbot-nginx/certbot_nginx/obj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index a559b5e02..f5ac88f6c 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -130,7 +130,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods self.names, self.ssl, self.enabled)) def __repr__(self): - return "VirtualHost(" + self.__str__().replace("\n",", ") + ")\n" + return "VirtualHost(" + self.__str__().replace("\n", ", ") + ")\n" def __eq__(self, other): if isinstance(other, self.__class__): From d2b4ae57404486f5d5b34aeaea6fc3637c263a75 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 18 Jun 2016 15:17:59 -0700 Subject: [PATCH 161/445] Consistently coerce inbound data to Unspacines --- certbot-nginx/certbot_nginx/nginxparser.py | 48 ++++++++++++---------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 6b3896fd9..d0f53e1a8 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -186,29 +186,37 @@ class UnspacedList(list): if "#" not in self[:i]: list.__delitem__(self, i) - def insert(self, i, x): - if not isinstance(x, list): # str or None - self.spaced.insert(i + self._spaces_before(i), x) + def _coerce(self, inbound): + """ + Coerce some inbound object to be appropriately usable in this object + + :param inbound: string or None or list or UnspacedList + :returns: (coerced UnspacedList or string or None, spaced equivalent) + :rtype: tuple + + """ + if not isinstance(inbound, list): # str or None + return (inbound, inbound) else: if not hasattr(x, "spaced"): - x = UnspacedList(x) - self.spaced.insert(i + self._spaces_before(i), x.spaced) - list.insert(self, i, x) + inbound = UnspacedList(inbound) + return (inbound, inbound.spaced) + + + def insert(self, i, x): + item, spaced_item = self._coerce(x) + self.spaced.insert(i + self._spaces_before(i), spaced_item) + list.insert(self, i, item) def append(self, x): - if hasattr(x, "spaced"): - self.spaced.append(x.spaced) - else: - self.spaced.append(x) - list.append(self, x) + item, spaced_item = self._coerce(x) + self.spaced.append(spaced_item) + list.append(self, item) def extend(self, x): - if hasattr(x, "spaced"): - self.spaced.extend(x.spaced) - else: - self.spaced.extend(x) - logger.debug("Weird, extending regular list %r to Unspaced %r", x, self) - list.extend(self, x) + item, spaced_item = self._coerce(x) + self.spaced.extend(spaced_item) + list.extend(self, item) def __add__(self, other): l = copy.deepcopy(self) @@ -216,10 +224,8 @@ class UnspacedList(list): return l def __setitem__(self, i, value): - if hasattr(value, "spaced"): - self.spaced.__setitem__(i + self._spaces_before(i), value.spaced) - else: - self.spaced.__setitem__(i + self._spaces_before(i), value) + item, spaced_item = self._coerce(x) + self.spaced.__setitem__(i + self._spaces_before(i), value) list.__setitem__(self, i, value) def __delitem__(self, i): From 5a872b829dfb774dd66d8a769c767534a183aae5 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 20 Jun 2016 08:57:51 +0300 Subject: [PATCH 162/445] Added tests --- .../certbot_apache/tests/configurator_test.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index e5c09fd1d..9a034c3e0 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -497,13 +497,8 @@ class MultipleVhostsTest(util.ApacheTest): # Test Listen statements with specific ip listeed self.config.prepare_server_https("443") - # Should only be 2 here, as the third interface - # already listens to the correct port - self.assertEqual(mock_add_dir.call_count, 2) - - # Check argument to new Listen statements - self.assertEqual(mock_add_dir.call_args_list[0][0][2], ["1.2.3.4:443"]) - self.assertEqual(mock_add_dir.call_args_list[1][0][2], ["[::1]:443"]) + # Should be 0 as one interface already listens to 443 + self.assertEqual(mock_add_dir.call_count, 0) # Reset return lists and inputs mock_add_dir.reset_mock() @@ -519,6 +514,28 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(mock_add_dir.call_args_list[2][0][2], ["1.1.1.1:8080", "https"]) + # mock_get.side_effect = ["1.2.3.4:80", "[::1]:80"] + # mock_find.return_value = ["test1", "test2", "test3"] + # self.config.parser.get_arg = mock_get + # self.config.prepare_server_https("8080", temp=True) + # self.assertEqual(self.listens, 0) + + def test_prepare_server_https_needed_listen(self): + mock_find = mock.Mock() + mock_find.return_value = ["test1", "test2"] + mock_get = mock.Mock() + mock_get.side_effect = ["1.2.3.4:8080", "80"] + mock_add_dir = mock.Mock() + mock_enable = mock.Mock() + + self.config.parser.find_dir = mock_find + self.config.parser.get_arg = mock_get + self.config.parser.add_dir_to_ifmodssl = mock_add_dir + self.config.enable_mod = mock_enable + + self.config.prepare_server_https("443") + self.assertEqual(mock_add_dir.call_count, 1) + def test_prepare_server_https_mixed_listen(self): mock_find = mock.Mock() From 418a5d501f8d18fc3b2dbfc48bb2643aab1dd728 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Mon, 20 Jun 2016 08:58:22 +0300 Subject: [PATCH 163/445] Refactored adding of listen statements --- certbot-apache/certbot_apache/configurator.py | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 2a7064190..9e404177a 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -625,6 +625,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ + # If nonstandard port, add service definition for matching + if port != "443": + port_service = "%s %s" % (port, "https") + else: + port_service = port + self.prepare_https_modules(temp) # Check for Listen # Note: This could be made to also look for ip:443 combo @@ -639,40 +645,56 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if self._has_port_already(listens, port): return + listen_dirs = set(listens) + for listen in listens: # For any listen statement, check if the machine also listens on # Port 443. If not, add such a listen statement. if len(listen.split(":")) == 1: # Its listening to all interfaces - if port not in listens: - if port == "443": - args = [port] - else: - # Non-standard ports should specify https protocol - args = [port, "https"] - self.parser.add_dir_to_ifmodssl( - parser.get_aug_path( - self.parser.loc["listen"]), "Listen", args) - self.save_notes += "Added Listen %s directive to %s\n" % ( - port, self.parser.loc["listen"]) - listens.append(port) + if port not in listen_dirs and port_service not in listen_dirs: + listen_dirs.add(port_service) else: # The Listen statement specifies an ip _, ip = listen[::-1].split(":", 1) ip = ip[::-1] - if "%s:%s" % (ip, port) not in listens: - if port == "443": - args = ["%s:%s" % (ip, port)] - else: - # Non-standard ports should specify https protocol - args = ["%s:%s" % (ip, port), "https"] - self.parser.add_dir_to_ifmodssl( - parser.get_aug_path( - self.parser.loc["listen"]), "Listen", args) - self.save_notes += ("Added Listen %s:%s directive to " - "%s\n") % (ip, port, - self.parser.loc["listen"]) - listens.append("%s:%s" % (ip, port)) + if "%s:%s" % (ip, port_service) not in listen_dirs and ( + "%s:%s" % (ip, port_service) not in listen_dirs): + listen_dirs.add("%s:%s" % (ip, port_service)) + self._add_listens(listen_dirs, listens, port) + + def _add_listens(self, listens, listens_orig, port): + """Helper method for prepare_server_https to figure out which new + listen statements need adding + + :param set listens: Set of all needed Listen statements + :param list listens_orig: List of existing listen statements + :param string port: Port number we're adding + """ + + # Add service definition for non-standard ports + if port != "443": + port_service = "%s %s" % (port, "https") + else: + port_service = port + + new_listens = listens.difference(listens_orig) + + if port in new_listens or port_service in new_listens: + # We have wildcard, skip the rest + self.parser.add_dir_to_ifmodssl( + parser.get_aug_path(self.parser.loc["listen"]), + "Listen", port_service.split(" ")) + self.save_notes += "Added Listen %s directive to %s\n" % ( + port_service, self.parser.loc["listen"]) + else: + for listen in new_listens: + self.parser.add_dir_to_ifmodssl( + parser.get_aug_path(self.parser.loc["listen"]), + "Listen", listen.split(" ")) + self.save_notes += ("Added Listen %s directive to " + "%s\n") % (listen, + self.parser.loc["listen"]) def _has_port_already(self, listens, port): """Helper method for prepare_server_https to find out if user From 0bfdea86d634109d63b12ac4ad593c76c04f1d80 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 20 Jun 2016 14:32:21 -0700 Subject: [PATCH 164/445] Bump cryptography version --- letsencrypt-auto-source/letsencrypt-auto | 45 ++++++++++--------- .../pieces/letsencrypt-auto-requirements.txt | 45 ++++++++++--------- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 80b81c898..fa3996e9e 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -598,28 +598,29 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.2.3 \ - --hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \ - --hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \ - --hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \ - --hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \ - --hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \ - --hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \ - --hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \ - --hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \ - --hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \ - --hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \ - --hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \ - --hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \ - --hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \ - --hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \ - --hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \ - --hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \ - --hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \ - --hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \ - --hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \ - --hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \ - --hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e +cryptography==1.3.4 \ + --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \ + --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \ + --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \ + --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \ + --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \ + --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \ + --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \ + --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \ + --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \ + --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \ + --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \ + --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \ + --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \ + --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \ + --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \ + --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \ + --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \ + --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \ + --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \ + --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \ + --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \ + --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 1291b2c96..2485491da 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -32,28 +32,29 @@ ConfigArgParse==0.10.0 \ --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 configobj==5.0.6 \ --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 -cryptography==1.2.3 \ - --hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \ - --hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \ - --hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \ - --hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \ - --hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \ - --hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \ - --hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \ - --hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \ - --hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \ - --hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \ - --hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \ - --hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \ - --hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \ - --hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \ - --hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \ - --hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \ - --hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \ - --hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \ - --hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \ - --hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \ - --hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e +cryptography==1.3.4 \ + --hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \ + --hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \ + --hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \ + --hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \ + --hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \ + --hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \ + --hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \ + --hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \ + --hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \ + --hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \ + --hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \ + --hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \ + --hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \ + --hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \ + --hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \ + --hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \ + --hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \ + --hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \ + --hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \ + --hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \ + --hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \ + --hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9 enum34==1.1.2 \ --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 From 8b3528969d4c0939e6f5cd4b4f77d873b2d50283 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 20 Jun 2016 14:33:00 -0700 Subject: [PATCH 165/445] Bump pyopenssl version --- letsencrypt-auto-source/letsencrypt-auto | 6 +++--- .../pieces/letsencrypt-auto-requirements.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index fa3996e9e..a25b3a70e 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -680,9 +680,9 @@ pyasn1==0.1.9 \ --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f -pyOpenSSL==0.15.1 \ - --hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \ - --hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672 +pyopenssl==16.0.0 \ + --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \ + --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d pyRFC3339==1.0 \ --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 2485491da..0c642a33e 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -114,9 +114,9 @@ pyasn1==0.1.9 \ --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f -pyOpenSSL==0.15.1 \ - --hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \ - --hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672 +pyopenssl==16.0.0 \ + --hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \ + --hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d pyRFC3339==1.0 \ --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 From ef31a837f7242691e8c6ae83f6a92a3cf6dc5792 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 15:56:19 -0700 Subject: [PATCH 166/445] Lint --- certbot-nginx/certbot_nginx/nginxparser.py | 8 ++++---- certbot-nginx/certbot_nginx/tests/util.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index d0f53e1a8..f74f3066e 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -198,7 +198,7 @@ class UnspacedList(list): if not isinstance(inbound, list): # str or None return (inbound, inbound) else: - if not hasattr(x, "spaced"): + if not hasattr(inbound, "spaced"): inbound = UnspacedList(inbound) return (inbound, inbound.spaced) @@ -224,9 +224,9 @@ class UnspacedList(list): return l def __setitem__(self, i, value): - item, spaced_item = self._coerce(x) - self.spaced.__setitem__(i + self._spaces_before(i), value) - list.__setitem__(self, i, value) + item, spaced_item = self._coerce(value) + self.spaced.__setitem__(i + self._spaces_before(i), spaced_item) + list.__setitem__(self, i, item) def __delitem__(self, i): self.spaced.__delitem__(i + self._spaces_before(i)) diff --git a/certbot-nginx/certbot_nginx/tests/util.py b/certbot-nginx/certbot_nginx/tests/util.py index 4ea4a03a7..866e5a9c7 100644 --- a/certbot-nginx/certbot_nginx/tests/util.py +++ b/certbot-nginx/certbot_nginx/tests/util.py @@ -17,7 +17,6 @@ from certbot.plugins import common from certbot_nginx import constants from certbot_nginx import configurator -from certbot_nginx import nginxparser class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods From e2fd1369f33dc8e8adbbf40562f5e90e3811bcb2 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 16:08:40 -0700 Subject: [PATCH 167/445] Update test for fancier semantics --- certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 2e8011443..1b1073de0 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -207,7 +207,7 @@ class TestUnspacedList(unittest.TestCase): self.assertEqual(ul3.spaced, self.a + self.b) self.assertEqual(self.ul.spaced, self.a) ul3 = self.ul + self.l2 - self.assertEqual(ul3, ["things", "quirk", "y", " "]) + self.assertEqual(ul3, ["things", "quirk", "y"]) self.assertEqual(ul3.spaced, self.a + self.b) def test_extend(self): From 02844904f0bd4a11f2d5f24e69033bbb580b0a9f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 16:11:32 -0700 Subject: [PATCH 168/445] Improve coverage --- certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 1b1073de0..4c239ac10 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -115,13 +115,7 @@ class TestRawNginxParser(unittest.TestCase): def test_dump_as_file(self): with open(util.get_data_filename('nginx.conf')) as handle: - try: - parsed = load(handle) - except: - handle.seek(0) - print "Failed on", handle.read() - raise - #parsed = util.filter_comments(parsed) + parsed = load(handle) parsed[-1][-1].append(UnspacedList([['server'], [['listen', ' ', '443 ssl'], ['server_name', ' ', 'localhost'], From 14ea8d8cdf8a63870b741886af3da21b946d9775 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 17:45:02 -0700 Subject: [PATCH 169/445] Fix server_name spaciness --- certbot-nginx/certbot_nginx/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 4f46b6a66..dd9a44aad 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -225,7 +225,7 @@ class NginxConfigurator(common.Plugin): if not matches: # No matches. Create a new vhost with this name in nginx.conf. filep = self.parser.loc["root"] - new_block = [['server'], [['server_name', target_name]]] + new_block = [['server'], [['\n', 'server_name', ' ', target_name]]] self.parser.add_http_directives(filep, new_block) vhost = obj.VirtualHost(filep, set([]), False, True, set([target_name]), list(new_block[1])) From 5960376d36f4ccec38814c87bf972c01a23f10b0 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 18:17:47 -0700 Subject: [PATCH 170/445] Cleanups --- certbot-nginx/certbot_nginx/nginxparser.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index f74f3066e..5c2bec577 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -62,9 +62,8 @@ class RawNginxParser(object): class RawNginxDumper(object): # pylint: disable=too-few-public-methods """A class that dumps nginx configuration from the provided tree.""" - def __init__(self, blocks, indentation=0): + def __init__(self, blocks): self.blocks = blocks - self.indentation = indentation def __iter__(self, blocks=None): """Iterates the dumped nginx content.""" @@ -100,10 +99,6 @@ class RawNginxDumper(object): if values and spacey(values): gap = values values = b.pop(0) - #if values is None: - # yield indentation + key + gap + ';' - #else: - # yield indentation + key + gap + values + ';' yield indentation + key + gap + values + ';' def __str__(self): From 56488b189901c2d9f760e241586cab7517f75d07 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 20 Jun 2016 18:18:25 -0700 Subject: [PATCH 171/445] Explain the most likely cause of a missing replay nonce error --- acme/acme/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/errors.py b/acme/acme/errors.py index 77d47c522..ca2ab1874 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -49,7 +49,7 @@ class MissingNonce(NonceError): def __str__(self): return ('Server {0} response did not include a replay ' - 'nonce, headers: {1}'.format( + 'nonce, headers: {1}\n(This may be a service outage)'.format( self.response.request.method, self.response.headers)) From 884b21ffbe3b577eb82f2da58793b31e7d5dd6b8 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 21 Jun 2016 15:11:32 -0700 Subject: [PATCH 172/445] fix docstring typo --- certbot-nginx/certbot_nginx/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 31595d56d..3c3942d22 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) class NginxParser(object): """Class handles the fine details of parsing the Nginx Configuration. - :ivar str root: Normalized abosulte path to the server root + :ivar str root: Normalized absolute path to the server root directory. Without trailing slash. :ivar dict parsed: Mapping of file paths to parsed trees From 098d23ac984d9c242dcaf3436ac1d13dac579769 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 21 Jun 2016 15:33:57 -0700 Subject: [PATCH 173/445] Comment a couple of things --- certbot-nginx/certbot_nginx/parser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index 3c3942d22..a0c29fc59 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -155,7 +155,9 @@ class NginxParser(object): :rtype: list """ - files = glob.glob(filepath) + files = glob.glob(filepath) # nginx on unix calls glob(3) for this + # XXX Windows nginx uses FindFirstFile, and + # should have a narrower call here trees = [] for item in files: if item in self.parsed and not override: @@ -210,6 +212,8 @@ class NginxParser(object): empty, this overrides the existing conf files. """ + # XXX probably this should be modified to perform atomic write + # operations and/or to defer control-c until completed for filename in self.parsed: tree = self.parsed[filename] if ext: From 7ea0ce5a1774bf3fadf1e5e2313bd3190a25a80f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 21 Jun 2016 15:34:26 -0700 Subject: [PATCH 174/445] Remove warning about nginx options file - This would be a security issue if we ran as setuid root at the request of non-privileged users, but we don't --- certbot-nginx/certbot_nginx/configurator.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 30928e56c..26640a527 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -689,11 +689,6 @@ def nginx_restart(nginx_ctl, nginx_conf="/etc/nginx.conf"): def temp_install(options_ssl): """Temporary install for convenience.""" - # WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY - # THIS SHOULD BE HANDLED BY THE PACKAGE MANAGER - # AND TAKEN OUT BEFORE RELEASE, INSTEAD - # SHOWING A NICE ERROR MESSAGE ABOUT THE PROBLEM. - # Check to make sure options-ssl.conf is installed if not os.path.isfile(options_ssl): shutil.copyfile(constants.MOD_SSL_CONF_SRC, options_ssl) From 1caf3e993517554d2cde2fdc4ab071bccf923c4b Mon Sep 17 00:00:00 2001 From: Dominic Cleal Date: Wed, 22 Jun 2016 09:29:37 +0100 Subject: [PATCH 175/445] Merge Augeas fix for comment line continuations From https://github.com/hercules-team/augeas/commit/64189250e2c49442a0875fa46dfb1d76a4371bde Fixes #2050 --- .../certbot_apache/augeas_lens/httpd.aug | 5 +- .../passing/comment-continuations-2050.conf | 428 ++++++++++++++++++ 2 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf diff --git a/certbot-apache/certbot_apache/augeas_lens/httpd.aug b/certbot-apache/certbot_apache/augeas_lens/httpd.aug index 7a5129b56..2729f4b60 100644 --- a/certbot-apache/certbot_apache/augeas_lens/httpd.aug +++ b/certbot-apache/certbot_apache/augeas_lens/httpd.aug @@ -52,11 +52,14 @@ let sep_eq = del /[ \t]*=[ \t]*/ "=" let nmtoken = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/ let word = /[a-z][a-z0-9._-]*/i -let comment = Util.comment let eol = Util.doseol let empty = Util.empty_dos let indent = Util.indent +let comment_val_re = /([^ \t\r\n](.|\\\\\r?\n)*[^ \\\t\r\n]|[^ \t\r\n])/ +let comment = [ label "#comment" . del /[ \t]*#[ \t]*/ "# " + . store comment_val_re . eol ] + (* borrowed from shellvars.aug *) let char_arg_dir = /([^\\ '"{\t\r\n]|[^ '"{\t\r\n]+[^\\ \t\r\n])|\\\\"|\\\\'|\\\\ / let char_arg_sec = /([^\\ '"\t\r\n>]|[^ '"\t\r\n>]+[^\\ \t\r\n>])|\\\\"|\\\\'|\\\\ / diff --git a/certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf b/certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf new file mode 100644 index 000000000..48b344d8a --- /dev/null +++ b/certbot-apache/certbot_apache/tests/apache-conf-files/passing/comment-continuations-2050.conf @@ -0,0 +1,428 @@ +# --------------------------------------------------------------- +# Core ModSecurity Rule Set ver.2.2.6 +# Copyright (C) 2006-2012 Trustwave All rights reserved. +# +# The OWASP ModSecurity Core Rule Set is distributed under +# Apache Software License (ASL) version 2 +# Please see the enclosed LICENCE file for full details. +# --------------------------------------------------------------- + + +# +# -- [[ Recommended Base Configuration ]] ------------------------------------------------- +# +# The configuration directives/settings in this file are used to control +# the OWASP ModSecurity CRS. These settings do **NOT** configure the main +# ModSecurity settings such as: +# +# - SecRuleEngine +# - SecRequestBodyAccess +# - SecAuditEngine +# - SecDebugLog +# +# You should use the modsecurity.conf-recommended file that comes with the +# ModSecurity source code archive. +# +# Ref: http://mod-security.svn.sourceforge.net/viewvc/mod-security/m2/trunk/modsecurity.conf-recommended +# + + +# +# -- [[ Rule Version ]] ------------------------------------------------------------------- +# +# Rule version data is added to the "Producer" line of Section H of the Audit log: +# +# - Producer: ModSecurity for Apache/2.7.0-rc1 (http://www.modsecurity.org/); OWASP_CRS/2.2.4. +# +# Ref: https://sourceforge.net/apps/mediawiki/mod-security/index.php?title=Reference_Manual#SecComponentSignature +# +#SecComponentSignature "OWASP_CRS/2.2.6" + + +# +# -- [[ Modes of Operation: Self-Contained vs. Collaborative Detection ]] ----------------- +# +# Each detection rule uses the "block" action which will inherit the SecDefaultAction +# specified below. Your settings here will determine which mode of operation you use. +# +# -- [[ Self-Contained Mode ]] -- +# Rules inherit the "deny" disruptive action. The first rule that matches will block. +# +# -- [[ Collaborative Detection Mode ]] -- +# This is a "delayed blocking" mode of operation where each matching rule will inherit +# the "pass" action and will only contribute to anomaly scores. Transactional blocking +# can be applied +# +# -- [[ Alert Logging Control ]] -- +# You have three options - +# +# - To log to both the Apache error_log and ModSecurity audit_log file use: "log" +# - To log *only* to the ModSecurity audit_log file use: "nolog,auditlog" +# - To log *only* to the Apache error_log file use: "log,noauditlog" +# +# Ref: http://blog.spiderlabs.com/2010/11/advanced-topic-of-the-week-traditional-vs-anomaly-scoring-detection-modes.html +# Ref: https://sourceforge.net/apps/mediawiki/mod-security/index.php?title=Reference_Manual#SecDefaultAction +# +#SecDefaultAction "phase:1,deny,log" + + +# +# -- [[ Collaborative Detection Severity Levels ]] ---------------------------------------- +# +# These are the default scoring points for each severity level. You may +# adjust these to you liking. These settings will be used in macro expansion +# in the rules to increment the anomaly scores when rules match. +# +# These are the default Severity ratings (with anomaly scores) of the individual rules - +# +# - 2: Critical - Anomaly Score of 5. +# Is the highest severity level possible without correlation. It is +# normally generated by the web attack rules (40 level files). +# - 3: Error - Anomaly Score of 4. +# Is generated mostly from outbound leakage rules (50 level files). +# - 4: Warning - Anomaly Score of 3. +# Is generated by malicious client rules (35 level files). +# - 5: Notice - Anomaly Score of 2. +# Is generated by the Protocol policy and anomaly files. +# +#SecAction \ + "id:'900001', \ + phase:1, \ + t:none, \ + setvar:tx.critical_anomaly_score=5, \ + setvar:tx.error_anomaly_score=4, \ + setvar:tx.warning_anomaly_score=3, \ + setvar:tx.notice_anomaly_score=2, \ + nolog, \ + pass" + + +# +# -- [[ Collaborative Detection Scoring Threshold Levels ]] ------------------------------ +# +# These variables are used in macro expansion in the 49 inbound blocking and 59 +# outbound blocking files. +# +# **MUST HAVE** ModSecurity v2.5.12 or higher to use macro expansion in numeric +# operators. If you have an earlier version, edit the 49/59 files directly to +# set the appropriate anomaly score levels. +# +# You should set the score to the proper threshold you would prefer. If set to "5" +# it will work similarly to previous Mod CRS rules and will create an event in the error_log +# file if there are any rules that match. If you would like to lessen the number of events +# generated in the error_log file, you should increase the anomaly score threshold to +# something like "20". This would only generate an event in the error_log file if +# there are multiple lower severity rule matches or if any 1 higher severity item matches. +# +#SecAction \ + "id:'900002', \ + phase:1, \ + t:none, \ + setvar:tx.inbound_anomaly_score_level=5, \ + nolog, \ + pass" + + +#SecAction \ + "id:'900003', \ + phase:1, \ + t:none, \ + setvar:tx.outbound_anomaly_score_level=4, \ + nolog, \ + pass" + + +# +# -- [[ Collaborative Detection Blocking ]] ----------------------------------------------- +# +# This is a collaborative detection mode where each rule will increment an overall +# anomaly score for the transaction. The scores are then evaluated in the following files: +# +# Inbound anomaly score - checked in the modsecurity_crs_49_inbound_blocking.conf file +# Outbound anomaly score - checked in the modsecurity_crs_59_outbound_blocking.conf file +# +# If you want to use anomaly scoring mode, then uncomment this line. +# +#SecAction \ + "id:'900004', \ + phase:1, \ + t:none, \ + setvar:tx.anomaly_score_blocking=on, \ + nolog, \ + pass" + + +# +# -- [[ GeoIP Database ]] ----------------------------------------------------------------- +# +# There are some rulesets that need to inspect the GEO data of the REMOTE_ADDR data. +# +# You must first download the MaxMind GeoIP Lite City DB - +# +# http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz +# +# You then need to define the proper path for the SecGeoLookupDb directive +# +# Ref: http://blog.spiderlabs.com/2010/10/detecting-malice-with-modsecurity-geolocation-data.html +# Ref: http://blog.spiderlabs.com/2010/11/detecting-malice-with-modsecurity-ip-forensics.html +# +#SecGeoLookupDb /opt/modsecurity/lib/GeoLiteCity.dat + +# +# -- [[ Regression Testing Mode ]] -------------------------------------------------------- +# +# If you are going to run the regression testing mode, you should uncomment the +# following rule. It will enable DetectionOnly mode for the SecRuleEngine and +# will enable Response Header tagging so that the client testing script can see +# which rule IDs have matched. +# +# You must specify the your source IP address where you will be running the tests +# from. +# +#SecRule REMOTE_ADDR "@ipMatch 192.168.1.100" \ + "id:'900005', \ + phase:1, \ + t:none, \ + ctl:ruleEngine=DetectionOnly, \ + setvar:tx.regression_testing=1, \ + nolog, \ + pass" + + +# +# -- [[ HTTP Policy Settings ]] ---------------------------------------------------------- +# +# Set the following policy settings here and they will be propagated to the 23 rules +# file (modsecurity_common_23_request_limits.conf) by using macro expansion. +# If you run into false positives, you can adjust the settings here. +# +# Only the max number of args is uncommented by default as there are a high rate +# of false positives. Uncomment the items you wish to set. +# +# +# -- Maximum number of arguments in request limited +#SecAction \ + "id:'900006', \ + phase:1, \ + t:none, \ + setvar:tx.max_num_args=255, \ + nolog, \ + pass" + +# +# -- Limit argument name length +#SecAction \ + "id:'900007', \ + phase:1, \ + t:none, \ + setvar:tx.arg_name_length=100, \ + nolog, \ + pass" + +# +# -- Limit value name length +#SecAction \ + "id:'900008', \ + phase:1, \ + t:none, \ + setvar:tx.arg_length=400, \ + nolog, \ + pass" + +# +# -- Limit arguments total length +#SecAction \ + "id:'900009', \ + phase:1, \ + t:none, \ + setvar:tx.total_arg_length=64000, \ + nolog, \ + pass" + +# +# -- Individual file size is limited +#SecAction \ + "id:'900010', \ + phase:1, \ + t:none, \ + setvar:tx.max_file_size=1048576, \ + nolog, \ + pass" + +# +# -- Combined file size is limited +#SecAction \ + "id:'900011', \ + phase:1, \ + t:none, \ + setvar:tx.combined_file_sizes=1048576, \ + nolog, \ + pass" + + +# +# Set the following policy settings here and they will be propagated to the 30 rules +# file (modsecurity_crs_30_http_policy.conf) by using macro expansion. +# If you run into false positves, you can adjust the settings here. +# +#SecAction \ + "id:'900012', \ + phase:1, \ + t:none, \ + setvar:'tx.allowed_methods=GET HEAD POST OPTIONS', \ + setvar:'tx.allowed_request_content_type=application/x-www-form-urlencoded|multipart/form-data|text/xml|application/xml|application/x-amf', \ + setvar:'tx.allowed_http_versions=HTTP/0.9 HTTP/1.0 HTTP/1.1', \ + setvar:'tx.restricted_extensions=.asa/ .asax/ .ascx/ .axd/ .backup/ .bak/ .bat/ .cdx/ .cer/ .cfg/ .cmd/ .com/ .config/ .conf/ .cs/ .csproj/ .csr/ .dat/ .db/ .dbf/ .dll/ .dos/ .htr/ .htw/ .ida/ .idc/ .idq/ .inc/ .ini/ .key/ .licx/ .lnk/ .log/ .mdb/ .old/ .pass/ .pdb/ .pol/ .printer/ .pwd/ .resources/ .resx/ .sql/ .sys/ .vb/ .vbs/ .vbproj/ .vsdisco/ .webinfo/ .xsd/ .xsx/', \ + setvar:'tx.restricted_headers=/Proxy-Connection/ /Lock-Token/ /Content-Range/ /Translate/ /via/ /if/', \ + nolog, \ + pass" + + +# +# -- [[ Content Security Policy (CSP) Settings ]] ----------------------------------------- +# +# The purpose of these settings is to send CSP response headers to +# Mozilla FireFox users so that you can enforce how dynamic content +# is used. CSP usage helps to prevent XSS attacks against your users. +# +# Reference Link: +# +# https://developer.mozilla.org/en/Security/CSP +# +# Uncomment this SecAction line if you want use CSP enforcement. +# You need to set the appropriate directives and settings for your site/domain and +# and activate the CSP file in the experimental_rules directory. +# +# Ref: http://blog.spiderlabs.com/2011/04/modsecurity-advanced-topic-of-the-week-integrating-content-security-policy-csp.html +# +#SecAction \ + "id:'900013', \ + phase:1, \ + t:none, \ + setvar:tx.csp_report_only=1, \ + setvar:tx.csp_report_uri=/csp_violation_report, \ + setenv:'csp_policy=allow \'self\'; img-src *.yoursite.com; media-src *.yoursite.com; style-src *.yoursite.com; frame-ancestors *.yoursite.com; script-src *.yoursite.com; report-uri %{tx.csp_report_uri}', \ + nolog, \ + pass" + + +# +# -- [[ Brute Force Protection ]] --------------------------------------------------------- +# +# If you are using the Brute Force Protection rule set, then uncomment the following +# lines and set the following variables: +# - Protected URLs: resources to protect (e.g. login pages) - set to your login page +# - Burst Time Slice Interval: time interval window to monitor for bursts +# - Request Threshold: request # threshold to trigger a burst +# - Block Period: temporary block timeout +# +#SecAction \ + "id:'900014', \ + phase:1, \ + t:none, \ + setvar:'tx.brute_force_protected_urls=/login.jsp /partner_login.php', \ + setvar:'tx.brute_force_burst_time_slice=60', \ + setvar:'tx.brute_force_counter_threshold=10', \ + setvar:'tx.brute_force_block_timeout=300', \ + nolog, \ + pass" + + +# +# -- [[ DoS Protection ]] ---------------------------------------------------------------- +# +# If you are using the DoS Protection rule set, then uncomment the following +# lines and set the following variables: +# - Burst Time Slice Interval: time interval window to monitor for bursts +# - Request Threshold: request # threshold to trigger a burst +# - Block Period: temporary block timeout +# +#SecAction \ + "id:'900015', \ + phase:1, \ + t:none, \ + setvar:'tx.dos_burst_time_slice=60', \ + setvar:'tx.dos_counter_threshold=100', \ + setvar:'tx.dos_block_timeout=600', \ + nolog, \ + pass" + + +# +# -- [[ Check UTF enconding ]] ----------------------------------------------------------- +# +# We only want to apply this check if UTF-8 encoding is actually used by the site, otherwise +# it will result in false positives. +# +# Uncomment this line if your site uses UTF8 encoding +#SecAction \ + "id:'900016', \ + phase:1, \ + t:none, \ + setvar:tx.crs_validate_utf8_encoding=1, \ + nolog, \ + pass" + + +# +# -- [[ Enable XML Body Parsing ]] ------------------------------------------------------- +# +# The rules in this file will trigger the XML parser upon an XML request +# +# Initiate XML Processor in case of xml content-type +# +#SecRule REQUEST_HEADERS:Content-Type "text/xml" \ + "id:'900017', \ + phase:1, \ + t:none,t:lowercase, \ + nolog, \ + pass, \ + chain" + #SecRule REQBODY_PROCESSOR "!@streq XML" \ + "ctl:requestBodyProcessor=XML" + + +# +# -- [[ Global and IP Collections ]] ----------------------------------------------------- +# +# Create both Global and IP collections for rules to use +# There are some CRS rules that assume that these two collections +# have already been initiated. +# +#SecRule REQUEST_HEADERS:User-Agent "^(.*)$" \ + "id:'900018', \ + phase:1, \ + t:none,t:sha1,t:hexEncode, \ + setvar:tx.ua_hash=%{matched_var}, \ + nolog, \ + pass" + + +#SecRule REQUEST_HEADERS:x-forwarded-for "^\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b" \ + "id:'900019', \ + phase:1, \ + t:none, \ + capture, \ + setvar:tx.real_ip=%{tx.1}, \ + nolog, \ + pass" + + +#SecRule &TX:REAL_IP "!@eq 0" \ + "id:'900020', \ + phase:1, \ + t:none, \ + initcol:global=global, \ + initcol:ip=%{tx.real_ip}_%{tx.ua_hash}, \ + nolog, \ + pass" + + +#SecRule &TX:REAL_IP "@eq 0" \ + "id:'900021', \ + phase:1, \ + t:none, \ + initcol:global=global, \ + initcol:ip=%{remote_addr}_%{tx.ua_hash}, \ + nolog, \ + pass" From 24cc6b208ae7fb869d3b93ef9d06c68f07a3e848 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 22 Jun 2016 15:24:33 -0700 Subject: [PATCH 176/445] Avoid newline --- acme/acme/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/errors.py b/acme/acme/errors.py index ca2ab1874..70894a808 100644 --- a/acme/acme/errors.py +++ b/acme/acme/errors.py @@ -49,7 +49,7 @@ class MissingNonce(NonceError): def __str__(self): return ('Server {0} response did not include a replay ' - 'nonce, headers: {1}\n(This may be a service outage)'.format( + 'nonce, headers: {1} (This may be a service outage)'.format( self.response.request.method, self.response.headers)) From 8a28cb7352eb5d534e79cdd213ede8fd8283dedb Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 22 Jun 2016 15:50:21 -0700 Subject: [PATCH 177/445] Implement Brad's more systematic solution for this --- certbot/display/util.py | 33 ++++++++++++++++++++++-------- certbot/tests/display/util_test.py | 1 + 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index 683dbc037..c998f78f3 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -29,7 +29,6 @@ CANCEL = "cancel" HELP = "help" """Display exit code when for when the user requests more help.""" - def _wrap_lines(msg): """Format lines nicely to 80 chars. @@ -51,6 +50,21 @@ def _wrap_lines(msg): return os.linesep.join(fixed_l) + +def _clean(dialog_result): + """Work around inconsistent return codes from python-dialog. + + :param tuple dialog_result: (code, result) + :returns: the argument but with unknown codes set to -1 (Error) + :rtype: tuple + """ + code, result = dialog_result + if code in (OK, HELP): + return dialog_result + else: + return (CANCEL, result) + + @zope.interface.implementer(interfaces.IDisplay) class NcursesDisplay(object): """Ncurses-based display.""" @@ -92,7 +106,7 @@ class NcursesDisplay(object): :param dict unused_kwargs: absorbs default / cli_args :returns: tuple of the form (`code`, `index`) where - `code` - int display exit code + `code` - display exit code `int` - index of the selected item :rtype: tuple @@ -111,7 +125,7 @@ class NcursesDisplay(object): # Can accept either tuples or just the actual choices if choices and isinstance(choices[0], tuple): # pylint: disable=star-args - code, selection = self.dialog.menu(message, **menu_options) + code, selection = _clean(self.dialog.menu(message, **menu_options)) # Return the selection index for i, choice in enumerate(choices): @@ -126,7 +140,7 @@ class NcursesDisplay(object): (str(i), choice) for i, choice in enumerate(choices, 1) ] # pylint: disable=star-args - code, index = self.dialog.menu(message, **menu_options) + code, index = _clean(self.dialog.menu(message, **menu_options)) if code == CANCEL or index == "": return code, -1 @@ -140,7 +154,7 @@ class NcursesDisplay(object): :param dict _kwargs: absorbs default / cli_args :returns: tuple of the form (`code`, `string`) where - `code` - int display exit code + `code` - display exit code `string` - input entered by the user """ @@ -148,7 +162,7 @@ class NcursesDisplay(object): # each section takes at least one line, plus extras if it's longer than self.width wordlines = [1 + (len(section) / self.width) for section in sections] height = 6 + sum(wordlines) + len(sections) - return self.dialog.inputbox(message, width=self.width, height=height) + return _clean(self.dialog.inputbox(message, width=self.width, height=height)) def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs): """Display a Yes/No dialog box. @@ -164,6 +178,7 @@ class NcursesDisplay(object): :rtype: bool """ + assert OK == self.dialog.DIALOG_OK, "What kind of absurdity is this?" return self.dialog.DIALOG_OK == self.dialog.yesno( message, self.height, self.width, yes_label=yes_label, no_label=no_label) @@ -179,7 +194,7 @@ class NcursesDisplay(object): :returns: tuple of the form (`code`, `list_tags`) where - `code` - int display exit code + `code` - display exit code `list_tags` - list of str tags selected by the user """ @@ -193,7 +208,7 @@ class NcursesDisplay(object): :param str message: prompt to give the user :returns: tuple of the form (`code`, `string`) where - `code` - int display exit code + `code` - display exit code `string` - input entered by the user """ @@ -355,7 +370,7 @@ class FileDisplay(object): :param str message: prompt to give the user :returns: tuple of the form (`code`, `string`) where - `code` - int display exit code + `code` - display exit code `string` - input entered by the user """ diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index 4a38803d1..94338118d 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -96,6 +96,7 @@ class NcursesDisplayTest(unittest.TestCase): @mock.patch("certbot.display.util." "dialog.Dialog.inputbox") def test_input(self, mock_input): + mock_input.return_value = (mock.MagicMock(), mock.MagicMock()) self.displayer.input("message") self.assertEqual(mock_input.call_count, 1) From 975599e9ca359586990c21a23f999eb93c243d51 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 22 Jun 2016 18:25:09 -0700 Subject: [PATCH 178/445] Remove bad save in nginx perform --- certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py | 2 +- certbot-nginx/certbot_nginx/tls_sni_01.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py index 3264d6ed3..a92caf788 100644 --- a/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py +++ b/certbot-nginx/certbot_nginx/tests/tls_sni_01_test.py @@ -80,7 +80,7 @@ class TlsSniPerformTest(util.NginxTest): mock_setup_cert.assert_called_once_with(self.achalls[0]) self.assertEqual([response], responses) - self.assertEqual(mock_save.call_count, 2) + self.assertEqual(mock_save.call_count, 1) # Make sure challenge config is included in main config http = self.sni.configurator.parser.parsed[ diff --git a/certbot-nginx/certbot_nginx/tls_sni_01.py b/certbot-nginx/certbot_nginx/tls_sni_01.py index e4c5d31a6..478810475 100644 --- a/certbot-nginx/certbot_nginx/tls_sni_01.py +++ b/certbot-nginx/certbot_nginx/tls_sni_01.py @@ -46,8 +46,6 @@ class NginxTlsSni01(common.TLSSNI01): if not self.achalls: return [] - self.configurator.save() - addresses = [] default_addr = "{0} default_server ssl".format( self.configurator.config.tls_sni_01_port) From 230b09bb15f5ba264b894b55634140e3c7002f4f Mon Sep 17 00:00:00 2001 From: Jacob Sachs Date: Thu, 23 Jun 2016 08:23:27 -0400 Subject: [PATCH 179/445] add logging for keeping existing cert --- certbot/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/main.py b/certbot/main.py index 7d6830b18..90fa62fad 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -83,6 +83,7 @@ def _auth_from_domains(le_client, config, domains, lineage=None): if action == "reinstall": # The lineage already exists; allow the caller to try installing # it without getting a new certificate at all. + logger.info("Keeping the existing certificate") return lineage, "reinstall" hooks.pre_hook(config) From 4ef7131a4b6868aa32370a27dd8f722a5ae2e429 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 23 Jun 2016 11:05:49 -0700 Subject: [PATCH 180/445] Name all of nginxparser's magic regexps --- certbot-nginx/certbot_nginx/nginxparser.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 5c2bec577..ab57089b9 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -4,7 +4,7 @@ import logging import string from pyparsing import ( - Literal, White, Word, alphanums, CharsNotIn, Forward, Group, + Literal, White, Word, alphanums, CharsNotIn, Combine, Forward, Group, Optional, OneOrMore, Regex, ZeroOrMore) from pyparsing import stringEnd from pyparsing import restOfLine @@ -17,10 +17,13 @@ class RawNginxParser(object): # constants space = Optional(White()) + nonspace = Regex(r"\S+") left_bracket = Literal("{").suppress() right_bracket = space.leaveWhitespace() + Literal("}").suppress() semicolon = Literal(";").suppress() key = Word(alphanums + "_/+-.") + dollar_var = Combine(Literal('$') + nonspace) + condition = Regex(r"\(.+\)") # Matches anything that is not a special character AND any chars in single # or double quotes value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") @@ -33,8 +36,8 @@ class RawNginxParser(object): assignment = space + key + Optional(space + value, default=None) + semicolon location_statement = space + Optional(modifier) + Optional(space + location + space) - if_statement = space + Literal("if") + space + Regex(r"\(.+\)") + space - map_statement = space + Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space + if_statement = space + Literal("if") + space + condition + space + map_statement = space + Literal("map") + space + nonspace + space + dollar_var + space block = Forward() block << Group( From db66050a7ae3048f4daf0a06ba6e87231abddcfb Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 23 Jun 2016 11:39:51 -0700 Subject: [PATCH 181/445] Make atomicity comment more accurate --- certbot-nginx/certbot_nginx/parser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index a0c29fc59..f2c10ab70 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -212,8 +212,7 @@ class NginxParser(object): empty, this overrides the existing conf files. """ - # XXX probably this should be modified to perform atomic write - # operations and/or to defer control-c until completed + # Best-effort atomicity is enforced above us by reverter.py for filename in self.parsed: tree = self.parsed[filename] if ext: From a29c6e3102ca1d093b7ffbaed93e88f635907e8d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 23 Jun 2016 14:06:32 -0700 Subject: [PATCH 182/445] Try simplifying handling of spacey sublists - this was causing test failures but they may all have been fixed by other changes... --- certbot-nginx/certbot_nginx/nginxparser.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index ab57089b9..efcdf94ef 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -172,12 +172,7 @@ class UnspacedList(list): for i, entry in reversed(list(enumerate(self))): if isinstance(entry, list): sublist = UnspacedList(entry) - if sublist != [] or sublist.spaced == []: - list.__setitem__(self, i, sublist) - else: - # if a sublist is exclusively spacey entries, it might - # choke the high level parser, so make it disappear - list.__delitem__(self, i) + list.__setitem__(self, i, sublist) self.spaced[i] = sublist.spaced elif spacey(entry): # don't delete comments From 3178a9f0f3365ef4665474a50ccab20b02ec8d91 Mon Sep 17 00:00:00 2001 From: Viktor Szakats Date: Fri, 24 Jun 2016 00:24:53 +0200 Subject: [PATCH 183/445] cli-help: fix spelling --- docs/cli-help.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli-help.txt b/docs/cli-help.txt index e5f1fdcb4..749983d0e 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -147,7 +147,7 @@ security: HTTPS for the newly authenticated vhost. (default: None) --hsts Add the Strict-Transport-Security header to every HTTP - response. Forcing browser to use always use SSL for + response. Forcing browser to always use SSL for the domain. Defends against SSL Stripping. (default: False) --no-hsts Do not automatically add the Strict-Transport-Security From cb441606d52fe3fd67fe8b3f2fd1672cd645ba49 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 16:14:34 -0700 Subject: [PATCH 184/445] Explictly state assumptions made by certbot --- certbot/interfaces.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 37835462e..a6f0b9735 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -180,6 +180,9 @@ class IAuthenticator(IPlugin): def cleanup(achalls): """Revert changes and shutdown after challenges complete. + This method should be able to revert all changes made by + perform, even if perform exited abnormally. + :param list achalls: Non-empty (guaranteed) list of :class:`~certbot.achallenges.AnnotatedChallenge` instances, a subset of those previously passed to :func:`perform`. @@ -304,8 +307,11 @@ class IInstaller(IPlugin): Both title and temporary are needed because a save may be intended to be permanent, but the save is not ready to be a full - checkpoint. If an exception is raised, it is assumed a new - checkpoint was not created. + checkpoint. + + It is assumed that at most one checkpoint is finalized by this + method. Additionally, if an exception is raised, it is assumed a + new checkpoint was not finalized. :param str title: The title of the save. If a title is given, the configuration will be saved as a new checkpoint and put in a From 38feb37d3ee0f9b0db689b0db2a2a9df1facd207 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 17:03:01 -0700 Subject: [PATCH 185/445] Further document reverter --- certbot/reverter.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/certbot/reverter.py b/certbot/reverter.py index f8140d60d..c7eab1690 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -24,6 +24,39 @@ logger = logging.getLogger(__name__) class Reverter(object): """Reverter Class - save and revert configuration checkpoints. + This class can be used by the plugins, especially installers, to + undo changes made to the user's system. Modifications to files and + commands to do undo actions taken by the plugin should be registered + with this class before the action is taken. + + Once a change has been registered with this class, there are three + states the change can be in. First, the change can be a temporary + change. This should be used for changes that will soon be reverted, + such as config changes for the purpose of solving a challenge. + Changes are added to this state through calls to + :func:`~add_to_temp_checkpoint` and reverted when + :func:`~revert_temporary_config` or :func:`~recovery_routine` is + called. + + The second state a change can be in is in progress. These changes + are not temporary, however, they also have not been finalized in a + checkpoint. A change must become in progress before it can be + finalized. Changes are added to this state through calls to + :func:`~add_to_checkpoint` and reverted when + :func:`~recovery_routine` is called. + + The last state a change can be in is finalized in a checkpoint. A + change is put into this state by first becoming an in progress + change and then calling :func:`~finalize_checkpoint`. Changes + in this state can be reverted through calls to + :func:`~rollback_checkpoints`. + + As a final note, creating new files and registering undo commands + are handled specially and use the methods + :func:`~register_file_creation` and :func:`~register_undo_command` + respectively. Both of these methods can be used to create either + temporary or in progress changes. + .. note:: Consider moving everything over to CSV format. :param config: Configuration. From 7e5e016982a0852f4151c434890102d733eedeba Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 17:10:03 -0700 Subject: [PATCH 186/445] Document that only save should make checkpoints --- certbot/interfaces.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index a6f0b9735..2bc25723c 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -241,6 +241,11 @@ class IInstaller(IPlugin): Represents any server that an X509 certificate can be placed. + It is assumed that :func:`save` is the only method that finalizes a + checkpoint. This is important to ensure that checkpoints are + restored in a consistent manner if requested by the user or in case + of an error. + """ def get_all_names(): From 7aa6becc5bffa19e85108b0286b08aa29aaf50aa Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 17:16:58 -0700 Subject: [PATCH 187/445] Suggest reverter for installers --- certbot/interfaces.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/certbot/interfaces.py b/certbot/interfaces.py index 2bc25723c..e4e62e0a2 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -246,6 +246,9 @@ class IInstaller(IPlugin): restored in a consistent manner if requested by the user or in case of an error. + Using :class:`certbot.reverter.Reverter` to implement checkpoints, + rollback, and recovery can dramatically simplify plugin development. + """ def get_all_names(): From 02c61cffb713ef3609b2dffd8e2465a95cd4fcfe Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 17:17:17 -0700 Subject: [PATCH 188/445] Consistent capitialization with installer --- certbot/reverter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/reverter.py b/certbot/reverter.py index c7eab1690..6dde05077 100644 --- a/certbot/reverter.py +++ b/certbot/reverter.py @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) class Reverter(object): """Reverter Class - save and revert configuration checkpoints. - This class can be used by the plugins, especially installers, to + This class can be used by the plugins, especially Installers, to undo changes made to the user's system. Modifications to files and commands to do undo actions taken by the plugin should be registered with this class before the action is taken. From bac988a4042537a2f6429e43b18553b1d0d9f710 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 23 Jun 2016 17:24:05 -0700 Subject: [PATCH 189/445] Fix typo --- certbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/cli.py b/certbot/cli.py index ba1f23708..35b3b74ae 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -773,7 +773,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis helpful.add( "security", "--hsts", action="store_true", help="Add the Strict-Transport-Security header to every HTTP response." - " Forcing browser to use always use SSL for the domain." + " Forcing browser to always use SSL for the domain." " Defends against SSL Stripping.", dest="hsts", default=False) helpful.add( "security", "--no-hsts", action="store_false", From 6930523c1469be5b9254405d045e897dfb85ee56 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 23 Jun 2016 17:59:26 -0700 Subject: [PATCH 190/445] Refactor to simplify indenation handling --- certbot-nginx/certbot_nginx/nginxparser.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index efcdf94ef..0ec1aeefb 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -41,7 +41,7 @@ class RawNginxParser(object): block = Forward() block << Group( - # XXX could this "key" be Literal("location")? + # key could for instance be "server" or "http" (Group(space + key + location_statement) ^ Group(if_statement) ^ Group(map_statement)).leaveWhitespace() + left_bracket + @@ -78,15 +78,14 @@ class RawNginxDumper(object): b = copy.deepcopy(b0) indentation = "" if spacey(b[0]): - indentation = b.pop(0) + yield b.pop(0) # indentation if not b: - yield indentation continue key = b.pop(0) values = b.pop(0) if isinstance(key, list): - yield indentation + "".join(key) + '{' + yield "".join(key) + '{' for parameter in values: dumped = self.__iter__([parameter]) for line in dumped: @@ -94,7 +93,7 @@ class RawNginxDumper(object): yield '}' else: if isinstance(key, str) and key.strip() == '#': - yield indentation + key + values + yield key + values else: gap = "" # Sometimes the parser has stuck some gap whitespace in here; @@ -102,7 +101,7 @@ class RawNginxDumper(object): if values and spacey(values): gap = values values = b.pop(0) - yield indentation + key + gap + values + ';' + yield key + gap + values + ';' def __str__(self): """Return the parsed block as a string.""" From ad13b525b28542378a8f506732b8ac1146d69b3e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 14:35:42 -0700 Subject: [PATCH 191/445] For reference, a buggy attempt to implement slicing but slice assignment seems to mangle the .spaced list in some cases --- certbot-nginx/certbot_nginx/nginxparser.py | 31 +++++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 0ec1aeefb..3db6ff501 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -76,7 +76,6 @@ class RawNginxDumper(object): yield b0 continue b = copy.deepcopy(b0) - indentation = "" if spacey(b[0]): yield b.pop(0) # indentation if not b: @@ -197,7 +196,7 @@ class UnspacedList(list): def insert(self, i, x): item, spaced_item = self._coerce(x) - self.spaced.insert(i + self._spaces_before(i), spaced_item) + self.spaced.insert(self._spaced_position(i), spaced_item) list.insert(self, i, item) def append(self, x): @@ -215,13 +214,28 @@ class UnspacedList(list): l.extend(other) return l + def __setslice__(self, i, j, newslice): + for idx in reversed(range(self._spaced_position(i), self._spaced_position(j))): + print "delspace", idx + del self.spaced[idx] + for idx in reversed(range(i,j)): + print "del", idx + list.__delitem__(self, idx) + for idx, item in enumerate(newslice): + self.insert(i + idx, item) + def __setitem__(self, i, value): + if isinstance(i, slice): + #raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") + for pos in xrange(i.start or 0, i.stop or len(self), i.step or 1): + self.__setitem__(pos, value) + return item, spaced_item = self._coerce(value) - self.spaced.__setitem__(i + self._spaces_before(i), spaced_item) + self.spaced.__setitem__(self._spaced_position(i), spaced_item) list.__setitem__(self, i, item) def __delitem__(self, i): - self.spaced.__delitem__(i + self._spaces_before(i)) + self.spaced.__delitem__(self._spaced_position(i)) list.__delitem__(self, i) def __deepcopy__(self, unused_memo): @@ -230,14 +244,17 @@ class UnspacedList(list): return l - def _spaces_before(self, idx): - "Count the number of spaces in the spaced list before pos idx in the spaceless one" + def _spaced_position(self, idx): + "Convert from indexes in the unspaced list to positions in the spaced one" + print "lookup", idx spaces = 0 pos = 0 + if idx < 0: + idx = len(self) + idx while idx != -1: if spacey(self.spaced[pos]): spaces += 1 else: idx -= 1 pos += 1 - return spaces + return idx + spaces From e415a4d402d73af1f71171bae844ed435363cacd Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 14:36:23 -0700 Subject: [PATCH 192/445] For now, instead, consider this NotImplemented --- certbot-nginx/certbot_nginx/nginxparser.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 3db6ff501..8a423848c 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -215,21 +215,11 @@ class UnspacedList(list): return l def __setslice__(self, i, j, newslice): - for idx in reversed(range(self._spaced_position(i), self._spaced_position(j))): - print "delspace", idx - del self.spaced[idx] - for idx in reversed(range(i,j)): - print "del", idx - list.__delitem__(self, idx) - for idx, item in enumerate(newslice): - self.insert(i + idx, item) + raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") def __setitem__(self, i, value): if isinstance(i, slice): - #raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") - for pos in xrange(i.start or 0, i.stop or len(self), i.step or 1): - self.__setitem__(pos, value) - return + raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") item, spaced_item = self._coerce(value) self.spaced.__setitem__(self._spaced_position(i), spaced_item) list.__setitem__(self, i, item) From 07fb5dd9cc2895799b370d5d25c56b9fca692e3a Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 24 Jun 2016 15:55:51 -0700 Subject: [PATCH 193/445] escape and unescape augeas --- certbot-apache/certbot_apache/configurator.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 57249eea5..33d79fadb 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -514,7 +514,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): try: args = self.aug.match(path + "/arg") except RuntimeError: - logger.warn("It looks like one of your paths has a character that your version of augeas can't parse") + logger.warn("Encountered a problem while parsing file: %s, skipping", path) return None for arg in args: addrs.add(obj.Addr.fromstring(self.parser.get_arg(arg))) @@ -529,7 +529,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if addr.get_port() == "443": is_ssl = True - filename = get_file_path(path) + filename = _unescape(get_file_path(path)) if self.conf("handle-sites"): is_enabled = self.is_site_enabled(filename) else: @@ -727,7 +727,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ avail_fp = nonssl_vhost.filep - ssl_fp = self._get_ssl_vhost_path(avail_fp) + ssl_fp = _escape(self._get_ssl_vhost_path(avail_fp)) self._copy_create_ssl_vhost_skeleton(avail_fp, ssl_fp) @@ -901,7 +901,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.parser.add_dir(vh_path, "Include", self.mod_ssl_conf) def _add_servername_alias(self, target_name, vhost): - fp = vhost.filep + fp = _escape(vhost.filep) vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" % (fp, parser.case_i("VirtualHost"))) if not vh_p: @@ -953,6 +953,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if need_to_save: self.save() + def _unescape(fp): + return fp.replace("\\", "") + + def _escape(fp): + return fp.replace(",", "\\,") ###################################################################### # Enhancements From 9da533d92e432d6ad365de9dbed7e2810491f577 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 16:13:12 -0700 Subject: [PATCH 194/445] Be more succinct --- certbot-nginx/certbot_nginx/nginxparser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 8a423848c..f65864df4 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -86,8 +86,7 @@ class RawNginxDumper(object): if isinstance(key, list): yield "".join(key) + '{' for parameter in values: - dumped = self.__iter__([parameter]) - for line in dumped: + for line in self.__iter__([parameter]): # negate "for b0 in blocks" yield line yield '}' else: From d67bc676813a0d67e73c23fd5f74b670d510db31 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 24 Jun 2016 16:17:09 -0700 Subject: [PATCH 195/445] add self --- certbot-apache/certbot_apache/configurator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 33d79fadb..f7d37cc40 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -529,7 +529,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if addr.get_port() == "443": is_ssl = True - filename = _unescape(get_file_path(path)) + filename = self._unescape(get_file_path(path)) if self.conf("handle-sites"): is_enabled = self.is_site_enabled(filename) else: @@ -727,7 +727,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ avail_fp = nonssl_vhost.filep - ssl_fp = _escape(self._get_ssl_vhost_path(avail_fp)) + ssl_fp = self._escape(self._get_ssl_vhost_path(avail_fp)) self._copy_create_ssl_vhost_skeleton(avail_fp, ssl_fp) @@ -901,7 +901,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.parser.add_dir(vh_path, "Include", self.mod_ssl_conf) def _add_servername_alias(self, target_name, vhost): - fp = _escape(vhost.filep) + fp = self._escape(vhost.filep) vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" % (fp, parser.case_i("VirtualHost"))) if not vh_p: @@ -953,10 +953,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if need_to_save: self.save() - def _unescape(fp): + + def _unescape(self, fp): return fp.replace("\\", "") - def _escape(fp): + def _escape(self, fp): return fp.replace(",", "\\,") ###################################################################### From 20b92a3c1fba3a922ea3063954b3691d4ed462d7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 16:48:22 -0700 Subject: [PATCH 196/445] Add some test coverage --- certbot-nginx/certbot_nginx/tests/configurator_test.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index a1d0e75cc..7529bc10b 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -265,7 +265,8 @@ class NginxConfiguratorTest(util.NginxTest): @mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform") @mock.patch("certbot_nginx.configurator.NginxConfigurator.restart") - def test_perform(self, mock_restart, mock_perform): + @mock.patch("certbot_nginx.configurator.NginxConfigurator.revert_challenge_config") + def test_perform_and_cleanup(self, mock_revert, mock_restart, mock_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( @@ -291,7 +292,11 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual(mock_perform.call_count, 1) self.assertEqual(responses, expected) - self.assertEqual(mock_restart.call_count, 1) + + self.config.cleanup([achall1, achall2]) + self.assertEqual(0, self.config._chall_out) # pylint: disable=protected-access + self.assertEqual(mock_revert.call_count, 1) + self.assertEqual(mock_restart.call_count, 2) @mock.patch("certbot_nginx.configurator.subprocess.Popen") def test_get_version(self, mock_popen): From 8f0a5fdc66f6b72d210a2edff32c27636bc3ecd7 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 16:48:30 -0700 Subject: [PATCH 197/445] Fix a bug in the new index calculations --- certbot-nginx/certbot_nginx/nginxparser.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index f65864df4..412268d22 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -235,15 +235,16 @@ class UnspacedList(list): def _spaced_position(self, idx): "Convert from indexes in the unspaced list to positions in the spaced one" - print "lookup", idx - spaces = 0 - pos = 0 + pos = spaces = 0 + # Normalize indexes like list[-1] etc, and save the result if idx < 0: idx = len(self) + idx + idx0 = idx + # Count the number of spaces in the spaced list before idx in the unspaced one while idx != -1: if spacey(self.spaced[pos]): spaces += 1 else: idx -= 1 pos += 1 - return idx + spaces + return idx0 + spaces From 6a938f2ee7ba48e891a783b0dd8880e0faa55883 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 16:52:02 -0700 Subject: [PATCH 198/445] Fix docstring nit --- certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 4c239ac10..1707bc6d6 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -176,7 +176,7 @@ class TestRawNginxParser(unittest.TestCase): ]) class TestUnspacedList(unittest.TestCase): - """Test the raw low-level Nginx config parser.""" + """Test the UnspacedList data structure""" def setUp(self): self.a = ["\n ", "things", " ", "quirk"] self.b = ["y", " "] From 880cb0319164d63ee6fed7a1e6cabcd4e46fd18c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 18:50:18 -0700 Subject: [PATCH 199/445] Make assertions about index policing --- certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index 1707bc6d6..a0ef82cd5 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -219,6 +219,10 @@ class TestUnspacedList(unittest.TestCase): self.assertEqual(ul3, ["zither", ["zather", "zest"]]) self.assertEqual(ul3.spaced, [self.a[0], "zither", " ", l]) + def test_get(self): + self.assertRaises(IndexError, self.ul2.__getitem__, 2) + self.assertRaises(IndexError, self.ul2.__getitem__, -3) + def test_rawlists(self): ul3 = copy.deepcopy(self.ul) ul3.insert(0, "some") From 0dc4639cbffe023520c18af375d2e909cdab0137 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 18:56:30 -0700 Subject: [PATCH 200/445] Be more explicit about range policing (rather than doing it in some roundabout crazy way) --- certbot-nginx/certbot_nginx/nginxparser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 412268d22..1de079cab 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -239,6 +239,8 @@ class UnspacedList(list): # Normalize indexes like list[-1] etc, and save the result if idx < 0: idx = len(self) + idx + if not 0 <= idx < len(self): + raise IndexError("list index out of range") idx0 = idx # Count the number of spaces in the spaced list before idx in the unspaced one while idx != -1: From 98d261596c554e31346f4aec69fd8356167e851c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 19:03:46 -0700 Subject: [PATCH 201/445] Raise NotImplemented for all problematic list methods --- certbot-nginx/certbot_nginx/nginxparser.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 1de079cab..065858797 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -213,6 +213,16 @@ class UnspacedList(list): l.extend(other) return l + def index(self, _, _j=0, _k=0): + raise NotImplementedError("UnspacedList.index() not yet implemented") + def pop(self, _): + raise NotImplementedError("UnspacedList.pop() not yet implemented") + def remove(self, _): + raise NotImplementedError("UnspacedList.remove() not yet implemented") + def reverse(self, _): + raise NotImplementedError("UnspacedList.reverse() not yet implemented") + def sort(self, _cmp=None, _key=None, _Rev=None): + raise NotImplementedError("UnspacedList.sort() not yet implemented") def __setslice__(self, i, j, newslice): raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") From 797d0a066072189c0745319bc8c4d524534e1d31 Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 15 Jun 2016 13:39:42 +0200 Subject: [PATCH 202/445] Printing pip output to terminal when -v is used Signed-off-by: Amjad Mashaal --- letsencrypt-auto-source/letsencrypt-auto.template | 12 +++++++++--- letsencrypt-auto-source/tests/auto_test.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index e8f313208..d838e976d 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -247,13 +247,19 @@ UNLIKELY_EOF # Set PATH so pipstrap upgrades the right (v)env: PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e - PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + if [ "$VERBOSE" = 1 ]; then + "$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1 + else + PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + fi PIP_STATUS=$? set -e if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) - echo "Had a problem while installing Python packages:" - echo "$PIP_OUT" + echo "Had a problem while installing Python packages." + if [ "$VERBOSE" != 1 ]; then + echo "$PIP_OUT" + fi rm -rf "$VENV_PATH" exit 1 fi diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 56023bc6f..8aea6603a 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -201,7 +201,7 @@ iQIDAQAB **kwargs) env.update(d) return out_and_err( - join(venv_dir, 'letsencrypt-auto') + ' --version', + join(venv_dir, 'letsencrypt-auto') + ' --verbose --version', shell=True, env=env) From f6069c2297364dbed088265745f9fd7ede7c4402 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 18:23:09 -0700 Subject: [PATCH 203/445] Explain what is happening when the user cancels vhost selection --- certbot-apache/certbot_apache/obj.py | 4 ++++ certbot-apache/certbot_apache/tls_sni_01.py | 1 + 2 files changed, 5 insertions(+) diff --git a/certbot-apache/certbot_apache/obj.py b/certbot-apache/certbot_apache/obj.py index b88b22428..c1af352cb 100644 --- a/certbot-apache/certbot_apache/obj.py +++ b/certbot-apache/certbot_apache/obj.py @@ -6,6 +6,7 @@ from certbot.plugins import common class Addr(common.Addr): """Represents an Apache address.""" + def __eq__(self, other): """This is defined as equalivalent within Apache. @@ -18,6 +19,9 @@ class Addr(common.Addr): self.is_wildcard() and other.is_wildcard())) return False + def __repr__(self): + return "certbot_apache.obj.Addr(" + repr(self.tup) + ")" + def __ne__(self, other): return not self.__eq__(other) diff --git a/certbot-apache/certbot_apache/tls_sni_01.py b/certbot-apache/certbot_apache/tls_sni_01.py index f14f7be0f..187ecd279 100644 --- a/certbot-apache/certbot_apache/tls_sni_01.py +++ b/certbot-apache/certbot_apache/tls_sni_01.py @@ -129,6 +129,7 @@ class ApacheTlsSni01(common.TLSSNI01): # because it's a new vhost that's not configured yet (GH #677), # or perhaps because there were multiple sections # in the config file (GH #1042). See also GH #2600. + logger.warn("Attempting to fall back to default vhost %s...", default_addr) addrs.add(default_addr) return addrs From e0691ede2c25ed361bee9cd7ae4e76515e65f564 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 18:43:29 -0700 Subject: [PATCH 204/445] Provide clear log messages when Apache tries a default vhost --- certbot-apache/certbot_apache/configurator.py | 9 ++++++--- certbot-apache/certbot_apache/display_ops.py | 9 +++++---- certbot-apache/certbot_apache/tls_sni_01.py | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 9e404177a..c51778ce5 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -327,9 +327,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): vhost = display_ops.select_vhost(target_name, self.vhosts) if vhost is None: logger.error( - "No vhost exists with servername or alias of: %s. " - "No vhost was selected. Please specify servernames " - "in the Apache config", target_name) + "No vhost exists with servername or alias of: %s " + "(or it's in a file with multiple files, which Certbot " + "can't parse yet). " + "No vhost was selected. Please specify ServerName or ServerAlias " + "in the Apache config, or split vhosts into separate files.", + target_name) raise errors.PluginError("No vhost selected") elif temp: return vhost diff --git a/certbot-apache/certbot_apache/display_ops.py b/certbot-apache/certbot_apache/display_ops.py index c9359e7d3..6b2964426 100644 --- a/certbot-apache/certbot_apache/display_ops.py +++ b/certbot-apache/certbot_apache/display_ops.py @@ -87,10 +87,11 @@ def _vhost_menu(domain, vhosts): "vhosts are not yet supported)".format(domain, os.linesep), choices, help_label="More Info", ok_label="Select") except errors.MissingCommandlineFlag as e: - msg = ("Failed to run Apache plugin non-interactively{1}{0}{1}" - "(The best solution is to add ServerName or ServerAlias " - "entries to the VirtualHost directives of your apache " - "configuration files.)".format(e, os.linesep)) + msg = ("Encountered vhost ambiguity but unable to ask for user guidance in " + "non-interactive mode. Currently Certbot needs each vhost to be " + "in its own conf file, and may need vhosts to be explicitly " + "labelled with ServerName or ServerAlias directories.") + logger.warn(msg) raise errors.MissingCommandlineFlag(msg) return code, tag diff --git a/certbot-apache/certbot_apache/tls_sni_01.py b/certbot-apache/certbot_apache/tls_sni_01.py index 187ecd279..32529485b 100644 --- a/certbot-apache/certbot_apache/tls_sni_01.py +++ b/certbot-apache/certbot_apache/tls_sni_01.py @@ -124,12 +124,12 @@ class ApacheTlsSni01(common.TLSSNI01): try: vhost = self.configurator.choose_vhost(achall.domain, temp=True) - except (PluginError, MissingCommandlineFlag): + except (PluginError, MissingCommandlineFlag), e: # We couldn't find the virtualhost for this domain, possibly # because it's a new vhost that's not configured yet (GH #677), # or perhaps because there were multiple sections # in the config file (GH #1042). See also GH #2600. - logger.warn("Attempting to fall back to default vhost %s...", default_addr) + logger.warn("Falling back to default vhost %s...", default_addr) addrs.add(default_addr) return addrs From e93ace79cc58063f34cc665c96cbcaa951478ebd Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 25 Jun 2016 10:51:14 -0700 Subject: [PATCH 205/445] Test coverage & fix --- certbot-apache/certbot_apache/display_ops.py | 2 +- certbot-apache/certbot_apache/tests/display_ops_test.py | 2 +- certbot-apache/certbot_apache/tests/obj_test.py | 3 +++ certbot-apache/certbot_apache/tls_sni_01.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/certbot-apache/certbot_apache/display_ops.py b/certbot-apache/certbot_apache/display_ops.py index 6b2964426..24aabaed4 100644 --- a/certbot-apache/certbot_apache/display_ops.py +++ b/certbot-apache/certbot_apache/display_ops.py @@ -86,7 +86,7 @@ def _vhost_menu(domain, vhosts): "like to choose?\n(note: conf files with multiple " "vhosts are not yet supported)".format(domain, os.linesep), choices, help_label="More Info", ok_label="Select") - except errors.MissingCommandlineFlag as e: + except errors.MissingCommandlineFlag: msg = ("Encountered vhost ambiguity but unable to ask for user guidance in " "non-interactive mode. Currently Certbot needs each vhost to be " "in its own conf file, and may need vhosts to be explicitly " diff --git a/certbot-apache/certbot_apache/tests/display_ops_test.py b/certbot-apache/certbot_apache/tests/display_ops_test.py index fd1e52fde..585661c7f 100644 --- a/certbot-apache/certbot_apache/tests/display_ops_test.py +++ b/certbot-apache/certbot_apache/tests/display_ops_test.py @@ -38,7 +38,7 @@ class SelectVhostTest(unittest.TestCase): try: self._call(self.vhosts) except errors.MissingCommandlineFlag as e: - self.assertTrue("VirtualHost directives" in e.message) + self.assertTrue("vhost ambiguity" in e.message) @mock.patch("certbot_apache.display_ops.zope.component.getUtility") def test_more_info_cancel(self, mock_util): diff --git a/certbot-apache/certbot_apache/tests/obj_test.py b/certbot-apache/certbot_apache/tests/obj_test.py index 4c3d331be..10dba18bc 100644 --- a/certbot-apache/certbot_apache/tests/obj_test.py +++ b/certbot-apache/certbot_apache/tests/obj_test.py @@ -22,6 +22,9 @@ class VirtualHostTest(unittest.TestCase): self.vhost2 = VirtualHost( "fp", "vhp", set([self.addr2]), False, False, "localhost") + def test_repr(self): + self.assertEqual(repr(self.addr2), "certbot_apache.obj.Addr(('127.0.0.1', '443'))") + def test_eq(self): self.assertTrue(self.vhost1b == self.vhost1) self.assertFalse(self.vhost1 == self.vhost2) diff --git a/certbot-apache/certbot_apache/tls_sni_01.py b/certbot-apache/certbot_apache/tls_sni_01.py index 32529485b..06d9e9ebd 100644 --- a/certbot-apache/certbot_apache/tls_sni_01.py +++ b/certbot-apache/certbot_apache/tls_sni_01.py @@ -124,7 +124,7 @@ class ApacheTlsSni01(common.TLSSNI01): try: vhost = self.configurator.choose_vhost(achall.domain, temp=True) - except (PluginError, MissingCommandlineFlag), e: + except (PluginError, MissingCommandlineFlag): # We couldn't find the virtualhost for this domain, possibly # because it's a new vhost that's not configured yet (GH #677), # or perhaps because there were multiple sections From 23f0ccbc8ee7f03b094c96fd2f776919b4f72d7c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 25 Jun 2016 12:22:45 -0700 Subject: [PATCH 206/445] Address review issues --- certbot/display/util.py | 21 ++++++++++++++++----- certbot/tests/display/util_test.py | 2 ++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index c998f78f3..4e7d45741 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -1,4 +1,5 @@ """Certbot display.""" +import logging import os import textwrap @@ -9,6 +10,9 @@ from certbot import interfaces from certbot import errors from certbot.display import completer + +logger = logging.getLogger(__name__) + WIDTH = 72 HEIGHT = 20 @@ -29,6 +33,10 @@ CANCEL = "cancel" HELP = "help" """Display exit code when for when the user requests more help.""" +ESC = "esc" +"""Display exit code when the user hits Escape""" + + def _wrap_lines(msg): """Format lines nicely to 80 chars. @@ -52,7 +60,7 @@ def _wrap_lines(msg): def _clean(dialog_result): - """Work around inconsistent return codes from python-dialog. + """Treat sundy python-dialog return codes as CANCEL :param tuple dialog_result: (code, result) :returns: the argument but with unknown codes set to -1 (Error) @@ -61,7 +69,10 @@ def _clean(dialog_result): code, result = dialog_result if code in (OK, HELP): return dialog_result + elif code in (CANCEL, ESC): + return (CANCEL, result) else: + logger.info("Surprising dialog return code %s", code) return (CANCEL, result) @@ -199,8 +210,8 @@ class NcursesDisplay(object): """ choices = [(tag, "", default_status) for tag in tags] - return self.dialog.checklist( - message, width=self.width, height=self.height, choices=choices) + return _clean(self.dialog.checklist( + message, width=self.width, height=self.height, choices=choices)) def directory_select(self, message, **unused_kwargs): """Display a directory selection screen. @@ -213,9 +224,9 @@ class NcursesDisplay(object): """ root_directory = os.path.abspath(os.sep) - return self.dialog.dselect( + return _clean(self.dialog.dselect( filepath=root_directory, width=self.width, - height=self.height, help_button=True, title=message) + height=self.height, help_button=True, title=message)) @zope.interface.implementer(interfaces.IDisplay) diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index 94338118d..a6ced90ab 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -113,6 +113,7 @@ class NcursesDisplayTest(unittest.TestCase): @mock.patch("certbot.display.util." "dialog.Dialog.checklist") def test_checklist(self, mock_checklist): + mock_checklist.return_value = (mock.MagicMock(), mock.MagicMock()) self.displayer.checklist("message", TAGS) choices = [ @@ -126,6 +127,7 @@ class NcursesDisplayTest(unittest.TestCase): @mock.patch("certbot.display.util.dialog.Dialog.dselect") def test_directory_select(self, mock_dselect): + mock_dselect.return_value = (mock.MagicMock(), mock.MagicMock()) self.displayer.directory_select("message") self.assertEqual(mock_dselect.call_count, 1) From 6f7ed85844c8842743228a5afb9163c7642225aa Mon Sep 17 00:00:00 2001 From: Amjad Mashaal Date: Wed, 25 May 2016 00:17:47 +0200 Subject: [PATCH 207/445] -v implies --text --- certbot/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/certbot/cli.py b/certbot/cli.py index 35b3b74ae..244d321d8 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -392,6 +392,11 @@ class HelpfulArgumentParser(object): ("Conflicting values for displayer." " {0} conflicts with dialog_mode").format(arg) ) + else: + # -v should imply --text + if (parsed_args.verbose_count > flag_default("verbose_count") and + not parsed_args.dialog_mode): + parsed_args.text_mode = True if parsed_args.validate_hooks: hooks.validate_hooks(parsed_args) From fdbb69930b53e8032dcb390b7c8c94e7f35408ec Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 27 Jun 2016 12:00:41 -0700 Subject: [PATCH 208/445] Tweak the NotImplementedError cases --- certbot-nginx/certbot_nginx/nginxparser.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 065858797..1f0569651 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -213,17 +213,15 @@ class UnspacedList(list): l.extend(other) return l - def index(self, _, _j=0, _k=0): - raise NotImplementedError("UnspacedList.index() not yet implemented") - def pop(self, _): + def pop(self, _i=0): raise NotImplementedError("UnspacedList.pop() not yet implemented") def remove(self, _): raise NotImplementedError("UnspacedList.remove() not yet implemented") - def reverse(self, _): + def reverse(self): raise NotImplementedError("UnspacedList.reverse() not yet implemented") def sort(self, _cmp=None, _key=None, _Rev=None): raise NotImplementedError("UnspacedList.sort() not yet implemented") - def __setslice__(self, i, j, newslice): + def __setslice__(self, _i, _j, _newslice): raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") def __setitem__(self, i, value): From 184f54cbc799039276ad2c7eb17a307b363ca9ac Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 27 Jun 2016 12:36:28 -0700 Subject: [PATCH 209/445] cosmetic improvements --- certbot-nginx/certbot_nginx/nginxparser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 1f0569651..ccf680dfc 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -41,7 +41,8 @@ class RawNginxParser(object): block = Forward() block << Group( - # key could for instance be "server" or "http" + # key could for instance be "server" or "http", or "location" (in which case + # location_statement needs to have a non-empty location) (Group(space + key + location_statement) ^ Group(if_statement) ^ Group(map_statement)).leaveWhitespace() + left_bracket + @@ -213,7 +214,7 @@ class UnspacedList(list): l.extend(other) return l - def pop(self, _i=0): + def pop(self, _i=None): raise NotImplementedError("UnspacedList.pop() not yet implemented") def remove(self, _): raise NotImplementedError("UnspacedList.remove() not yet implemented") From 6017a6cb6daca4a0eddd7a85b874f66e26d9d78c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 24 Jun 2016 17:14:14 -0700 Subject: [PATCH 210/445] Only write nginx config files if we've modified them --- certbot-nginx/certbot_nginx/nginxparser.py | 17 +++++++++++++++-- certbot-nginx/certbot_nginx/parser.py | 5 ++++- .../certbot_nginx/tests/parser_test.py | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index ccf680dfc..816507738 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -163,6 +163,7 @@ class UnspacedList(list): def __init__(self, list_source): # ensure our argument is not a generator, and duplicate any sublists self.spaced = copy.deepcopy(list(list_source)) + self.dirty = False # Turn self into a version of the source list that has spaces removed # and all sub-lists also UnspacedList()ed @@ -198,20 +199,24 @@ class UnspacedList(list): item, spaced_item = self._coerce(x) self.spaced.insert(self._spaced_position(i), spaced_item) list.insert(self, i, item) + self.dirty = True def append(self, x): item, spaced_item = self._coerce(x) self.spaced.append(spaced_item) list.append(self, item) + self.dirty = True def extend(self, x): item, spaced_item = self._coerce(x) self.spaced.extend(spaced_item) list.extend(self, item) + self.dirty = True def __add__(self, other): l = copy.deepcopy(self) l.extend(other) + l.dirty = True return l def pop(self, _i=None): @@ -231,16 +236,24 @@ class UnspacedList(list): item, spaced_item = self._coerce(value) self.spaced.__setitem__(self._spaced_position(i), spaced_item) list.__setitem__(self, i, item) + self.dirty = True def __delitem__(self, i): self.spaced.__delitem__(self._spaced_position(i)) list.__delitem__(self, i) + self.dirty = True - def __deepcopy__(self, unused_memo): + def __deepcopy__(self, memo): l = UnspacedList(self[:]) - l.spaced = copy.deepcopy(self.spaced) + l.spaced = copy.deepcopy(self.spaced, memo=memo) + l.dirty = self.dirty return l + def is_dirty(self): + """Recurse through the parse tree to figure out if any sublists are dirty""" + if self.dirty: + return True + return any((isinstance(x, list) and x.is_dirty() for x in self)) def _spaced_position(self, idx): "Convert from indexes in the unspaced list to positions in the spaced one" diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index f2c10ab70..e7992810a 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -205,11 +205,12 @@ class NginxParser(object): raise errors.NoInstallationError( "Could not find configuration root") - def filedump(self, ext='tmp'): + def filedump(self, ext='tmp', lazy=True): """Dumps parsed configurations into files. :param str ext: The file extension to use for the dumped files. If empty, this overrides the existing conf files. + :param bool lazy: Only write files that have been modified """ # Best-effort atomicity is enforced above us by reverter.py @@ -218,6 +219,8 @@ class NginxParser(object): if ext: filename = filename + os.path.extsep + ext try: + if lazy and not tree.is_dirty(): + continue out = nginxparser.dumps(tree) logger.debug('Writing nginx conf tree to %s:\n%s', filename, out) with open(filename, 'w') as _file: diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index 6d046178a..464d717ed 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -66,7 +66,7 @@ class NginxParserTest(util.NginxTest): def test_filedump(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) - nparser.filedump('test') + nparser.filedump('test', lazy=False) # pylint: disable=protected-access parsed = nparser._parse_files(nparser.abs_path( 'sites-enabled/example.com.test')) From 3e9bf2f35f6fc91e7b56109d3ee81685970b2271 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 23 Jun 2016 14:03:28 -0700 Subject: [PATCH 211/445] Tests for UnspacedList.is_dirty() --- certbot-nginx/certbot_nginx/tests/nginxparser_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py index a0ef82cd5..a0a0736a5 100644 --- a/certbot-nginx/certbot_nginx/tests/nginxparser_test.py +++ b/certbot-nginx/certbot_nginx/tests/nginxparser_test.py @@ -231,6 +231,17 @@ class TestUnspacedList(unittest.TestCase): del ul3[2] self.assertEqual(ul3, ["some", "things", "why", "did", "whether"]) + def test_is_dirty(self): + self.assertEqual(False, self.ul2.is_dirty()) + ul3 = UnspacedList([]) + ul3.append(self.ul) + self.assertEqual(False, self.ul.is_dirty()) + self.assertEqual(True, ul3.is_dirty()) + ul4 = UnspacedList([[1], [2, 3, 4]]) + self.assertEqual(False, ul4.is_dirty()) + ul4[1][2] = 5 + self.assertEqual(True, ul4.is_dirty()) + if __name__ == '__main__': unittest.main() # pragma: no cover From bdbdd826de3ec8cecf3834e7b44848faa93b3721 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 27 Jun 2016 12:44:53 -0700 Subject: [PATCH 212/445] Extra comment & concision --- certbot-nginx/certbot_nginx/nginxparser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 816507738..d6c352296 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -1,4 +1,5 @@ """Very low-level nginx config parser based on pyparsing.""" +# Forked from https://github.com/fatiherikli/nginxparser (MIT Licensed) import copy import logging import string @@ -81,8 +82,7 @@ class RawNginxDumper(object): yield b.pop(0) # indentation if not b: continue - key = b.pop(0) - values = b.pop(0) + key, values = b.pop(0), b.pop(0) if isinstance(key, list): yield "".join(key) + '{' @@ -91,9 +91,9 @@ class RawNginxDumper(object): yield line yield '}' else: - if isinstance(key, str) and key.strip() == '#': + if isinstance(key, str) and key.strip() == '#': # comment yield key + values - else: + else: # assignment gap = "" # Sometimes the parser has stuck some gap whitespace in here; # if so rotate it into gap From bbd5ce45e90ba6bb0aaf3d55dcf87c9f2ad7552d Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 28 Jun 2016 13:49:44 -0500 Subject: [PATCH 213/445] Defer signals in ErrorHandler. --- certbot/error_handler.py | 46 ++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index 431e677a1..5b9220583 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -19,6 +19,14 @@ _SIGNALS = ([signal.SIGTERM] if os.name == "nt" else signal.SIGXCPU, signal.SIGXFSZ]) +class SignalExit(Exception): + """This exception is used stop of execution of the code in the body of the + ErrorHandler context manager. + + """ + pass + + class ErrorHandler(object): """Registers functions to be called if an exception or signal occurs. @@ -34,16 +42,17 @@ class ErrorHandler(object): if a signal is encountered, cleanup_func is called followed by the previously registered signal handler. - Every registered function is attempted to be run to completion - exactly once. If a registered function raises an exception, it is - logged and the next function is called. If a (different) handled - signal occurs while calling a registered function, it is attempted - to be called again by the next signal handler. + Each registered cleanup function is called exactly once. If a registered + function raises an exception, it is logged and the next function is called. + Signals received while the registered functions are executing are + deferred until they finish. """ def __init__(self, func=None, *args, **kwargs): + self.body_executed = False self.funcs = [] self.prev_handlers = {} + self.received_signals = [] if func is not None: self.register(func, *args, **kwargs) @@ -51,12 +60,22 @@ class ErrorHandler(object): self.set_signal_handlers() def __exit__(self, exec_type, exec_value, trace): + self.body_executed = True + retval = False + if exec_type is SignalExit: + logger.debug("Encountered signals: %s", self.received_signals) + self.call_registered() + for signum in self.received_signals: + self.call_signal(signum) + retval = True # SystemExit is ignored to properly handle forks that don't exec - if exec_type not in (None, SystemExit): + elif exec_type not in (None, SystemExit): logger.debug("Encountered exception:\n%s", "".join( traceback.format_exception(exec_type, exec_value, trace))) self.call_registered() + self.reset_signal_handlers() + return retval def register(self, func, *args, **kwargs): """Sets func to be called with *args and **kwargs during cleanup @@ -93,12 +112,21 @@ class ErrorHandler(object): self.prev_handlers.clear() def _signal_handler(self, signum, unused_frame): - """Calls registered functions and the previous signal handler. + """Stores the recieved signal. :param int signum: number of current signal """ - logger.debug("Singal %s encountered", signum) - self.call_registered() + self.received_signals.append(signum) + if not self.body_executed: + raise SignalExit + + def call_signal(self, signum): + """Calls the signal given by signum. + + :param int signum: signal number + + """ + logger.debug("Calling signal %s", signum) signal.signal(signum, self.prev_handlers[signum]) os.kill(os.getpid(), signum) From 1356941f9760c4cda79097716c6cf86d0d179e07 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 28 Jun 2016 13:58:26 -0500 Subject: [PATCH 214/445] Test signal handling in error handler. --- certbot/tests/error_handler_test.py | 61 +++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index 5434b36be..c2551195a 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -1,11 +1,33 @@ """Tests for certbot.error_handler.""" +import os import signal import sys import unittest +from contextlib import contextmanager import mock +@contextmanager +def signal_receiver(signums): + """Context manager to catch signals""" + def receiver(signum, unused_frame): + signals.append(signum) + signals = [] + prev_handlers = {} + for signum in signums: + prev_handlers[signum] = signal.getsignal(signum) + signal.signal(signum, receiver) + yield signals + for signum in signums: + signal.signal(signum, prev_handlers[signum]) + + +def send_signal(signum): + """Send the given signal""" + os.kill(os.getpid(), signum) + + class ErrorHandlerTest(unittest.TestCase): """Tests for certbot.error_handler.""" @@ -30,25 +52,20 @@ class ErrorHandlerTest(unittest.TestCase): self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) - @mock.patch('certbot.error_handler.os') - @mock.patch('certbot.error_handler.signal') - def test_signal_handler(self, mock_signal, mock_os): - # pylint: disable=protected-access - mock_signal.getsignal.return_value = signal.SIG_DFL - self.handler.set_signal_handlers() - signal_handler = self.handler._signal_handler - for signum in self.signals: - mock_signal.signal.assert_any_call(signum, signal_handler) + def test_context_manager_with_signal(self): + with signal_receiver(self.signals) as signals_received: + with self.handler: + should_be_42 = 42 + send_signal(signal.SIGTERM) + should_be_42 *= 10 - signum = self.signals[0] - signal_handler(signum, None) + # check exectuion stoped when the signal was sent + assert 42 == should_be_42 + # assert signals were caught + assert [signal.SIGTERM] == signals_received + # assert the error handling function was just called once self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) - mock_os.kill.assert_called_once_with(mock_os.getpid(), signum) - - self.handler.reset_signal_handlers() - for signum in self.signals: - mock_signal.signal.assert_any_call(signum, signal.SIG_DFL) def test_bad_recovery(self): bad_func = mock.MagicMock(side_effect=[ValueError]) @@ -58,6 +75,18 @@ class ErrorHandlerTest(unittest.TestCase): **self.init_kwargs) bad_func.assert_called_once_with() + def test_bad_recovery_with_signal(self): + bad_func = mock.MagicMock( + side_effect=lambda: send_signal(signal.SIGHUP)) + self.handler.register(bad_func) + with signal_receiver(self.signals) as signals_received: + with self.handler: + send_signal(signal.SIGTERM) + assert [signal.SIGTERM, signal.SIGHUP] == signals_received + self.init_func.assert_called_once_with(*self.init_args, + **self.init_kwargs) + bad_func.assert_called_once_with() + def test_sysexit_ignored(self): try: with self.handler: From e6521414499247570e8e252ad5bc1d32fbdf33c3 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 28 Jun 2016 16:55:46 -0500 Subject: [PATCH 215/445] Use lambda instead of closure. --- certbot/tests/error_handler_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index c2551195a..f8ac0718b 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -11,13 +11,11 @@ import mock @contextmanager def signal_receiver(signums): """Context manager to catch signals""" - def receiver(signum, unused_frame): - signals.append(signum) signals = [] prev_handlers = {} for signum in signums: prev_handlers[signum] = signal.getsignal(signum) - signal.signal(signum, receiver) + signal.signal(signum, lambda signum, _: signals.append(signum)) yield signals for signum in signums: signal.signal(signum, prev_handlers[signum]) From 78b30539faa87fbeb6edea99d5685173a2439505 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 28 Jun 2016 17:56:31 -0700 Subject: [PATCH 216/445] augeas tests --- .../certbot_apache/tests/configurator_test.py | 33 +++ .../augeas_vhosts/apache2/apache2.conf | 196 ++++++++++++++++++ .../apache2/conf-available/bad_conf_file.conf | 3 + .../other-vhosts-access-log.conf | 4 + .../apache2/conf-available/security.conf | 35 ++++ .../apache2/conf-available/serve-cgi-bin.conf | 20 ++ .../conf-enabled/other-vhosts-access-log.conf | 1 + .../apache2/conf-enabled/security.conf | 1 + .../apache2/conf-enabled/serve-cgi-bin.conf | 1 + .../augeas_vhosts/apache2/envvars | 29 +++ .../apache2/mods-available/authz_svn.load | 5 + .../apache2/mods-available/dav.load | 3 + .../apache2/mods-available/dav_svn.conf | 56 +++++ .../apache2/mods-available/dav_svn.load | 7 + .../apache2/mods-available/rewrite.load | 1 + .../apache2/mods-available/ssl.conf | 89 ++++++++ .../apache2/mods-available/ssl.load | 2 + .../apache2/mods-enabled/.gitignore | 0 .../apache2/mods-enabled/authz_svn.load | 1 + .../apache2/mods-enabled/dav.load | 1 + .../apache2/mods-enabled/dav_svn.conf | 1 + .../apache2/mods-enabled/dav_svn.load | 1 + .../augeas_vhosts/apache2/ports.conf | 15 ++ .../apache2/sites-available/000-default.conf | 12 ++ .../apache2/sites-available/certbot.conf | 42 ++++ .../default-ssl-port-only.conf | 36 ++++ .../apache2/sites-available/default-ssl.conf | 40 ++++ .../sites-available/encryption-example.conf | 42 ++++ .../sites-available/mod_macro-example.conf | 15 ++ .../apache2/sites-available/ocsp-ssl.conf | 36 ++++ .../apache2/sites-available/old,default.conf | 12 ++ .../apache2/sites-available/wildcard.conf | 13 ++ .../apache2/sites-enabled/000-default.conf | 1 + .../apache2/sites-enabled/certbot.conf | 1 + .../sites-enabled/encryption-example.conf | 1 + .../sites-enabled/mod_macro-example.conf | 1 + .../apache2/sites-enabled/ocsp-ssl.conf | 1 + .../debian_apache_2_4/augeas_vhosts/sites | 3 + 38 files changed, 761 insertions(+) create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/apache2.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/bad_conf_file.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/other-vhosts-access-log.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/security.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/serve-cgi-bin.conf create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/other-vhosts-access-log.conf create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/security.conf create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/serve-cgi-bin.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/envvars create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/authz_svn.load create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav.load create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav_svn.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav_svn.load create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/rewrite.load create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/ssl.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/ssl.load create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/.gitignore create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/authz_svn.load create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav.load create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav_svn.conf create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav_svn.load create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/ports.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/000-default.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/certbot.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl-port-only.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/encryption-example.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/mod_macro-example.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/ocsp-ssl.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/old,default.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/000-default.conf create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/certbot.conf create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/encryption-example.conf create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/mod_macro-example.conf create mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/ocsp-ssl.conf create mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/sites diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index e5c09fd1d..fca62a626 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -1169,6 +1169,39 @@ class MultipleVhostsTest(util.ApacheTest): self.config.aug.match.side_effect = RuntimeError self.assertFalse(self.config._check_aug_version()) +class AugeasVhostsTest(util.ApacheTest): + """Test vhosts with illegal names dependant on augeas version.""" + + def setUp(self): # pylint: disable=arguments-differ + super(AugeasVhostsTest, self).setUp(test_dir="debian_apache_2_4/augeas_vhosts", + config_root="debian_apache_2_4/augeas_vhosts/apache2", + vhost_root="debian_apache_2_4/augeas_vhosts/apache2/sites-available") + + self.config = util.get_apache_configurator( + self.config_path, self.vhost_path, self.config_dir, self.work_dir) + self.config = self.mock_deploy_cert(self.config) + self.vh_truth = util.get_vh_truth( + self.temp_dir, "debian_apache_2_4/augeas_vhosts") + + def mock_deploy_cert(self, config): + """A test for a mock deploy cert""" + self.config.real_deploy_cert = self.config.deploy_cert + + def mocked_deploy_cert(*args, **kwargs): + """a helper to mock a deployed cert""" + with mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod"): + config.real_deploy_cert(*args, **kwargs) + self.config.deploy_cert = mocked_deploy_cert + return self.config + + def tearDown(self): + shutil.rmtree(self.temp_dir) + shutil.rmtree(self.config_dir) + shutil.rmtree(self.work_dir) + + def test_choosevhost_with_illegal_name(self): + chosen_vhost = self.config._create_vhost("debian_apache_2_4/augeas_vhosts/apache2/sites-available/old,default.conf") + self.assertEqual(None, chosen_vhost) if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/apache2.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/apache2.conf new file mode 100644 index 000000000..2a5bb7be2 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/apache2.conf @@ -0,0 +1,196 @@ +# This is the main Apache server configuration file. It contains the +# configuration directives that give the server its instructions. +# See http://httpd.apache.org/docs/2.4/ for detailed information about +# the directives and /usr/share/doc/apache2/README.Debian about Debian specific +# hints. +# +# +# Summary of how the Apache 2 configuration works in Debian: +# The Apache 2 web server configuration in Debian is quite different to +# upstream's suggested way to configure the web server. This is because Debian's +# default Apache2 installation attempts to make adding and removing modules, +# virtual hosts, and extra configuration directives as flexible as possible, in +# order to make automating the changes and administering the server as easy as +# possible. + +# It is split into several files forming the configuration hierarchy outlined +# below, all located in the /etc/apache2/ directory: +# +# /etc/apache2/ +# |-- apache2.conf +# | `-- ports.conf +# |-- mods-enabled +# | |-- *.load +# | `-- *.conf +# |-- conf-enabled +# | `-- *.conf +# `-- sites-enabled +# `-- *.conf +# +# +# * apache2.conf is the main configuration file (this file). It puts the pieces +# together by including all remaining configuration files when starting up the +# web server. +# +# * ports.conf is always included from the main configuration file. It is +# supposed to determine listening ports for incoming connections which can be +# customized anytime. +# +# * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/ +# directories contain particular configuration snippets which manage modules, +# global configuration fragments, or virtual host configurations, +# respectively. +# +# They are activated by symlinking available configuration files from their +# respective *-available/ counterparts. These should be managed by using our +# helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See +# their respective man pages for detailed information. +# +# * The binary is called apache2. Due to the use of environment variables, in +# the default configuration, apache2 needs to be started/stopped with +# /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not +# work with the default configuration. + + +# Global configuration + +# +# The accept serialization lock file MUST BE STORED ON A LOCAL DISK. +# +Mutex file:${APACHE_LOCK_DIR} default + +# +# PidFile: The file in which the server should record its process +# identification number when it starts. +# This needs to be set in /etc/apache2/envvars +# +PidFile ${APACHE_PID_FILE} + +# +# Timeout: The number of seconds before receives and sends time out. +# +Timeout 300 + +# +# KeepAlive: Whether or not to allow persistent connections (more than +# one request per connection). Set to "Off" to deactivate. +# +KeepAlive On + +# +# MaxKeepAliveRequests: The maximum number of requests to allow +# during a persistent connection. Set to 0 to allow an unlimited amount. +# We recommend you leave this number high, for maximum performance. +# +MaxKeepAliveRequests 100 + +# +# KeepAliveTimeout: Number of seconds to wait for the next request from the +# same client on the same connection. +# +KeepAliveTimeout 5 + + +# These need to be set in /etc/apache2/envvars +User ${APACHE_RUN_USER} +Group ${APACHE_RUN_GROUP} + +# +# HostnameLookups: Log the names of clients or just their IP addresses +# e.g., www.apache.org (on) or 204.62.129.132 (off). +# The default is off because it'd be overall better for the net if people +# had to knowingly turn this feature on, since enabling it means that +# each client request will result in AT LEAST one lookup request to the +# nameserver. +# +HostnameLookups Off + +# ErrorLog: The location of the error log file. +# If you do not specify an ErrorLog directive within a +# container, error messages relating to that virtual host will be +# logged here. If you *do* define an error logfile for a +# container, that host's errors will be logged there and not here. +# +ErrorLog ${APACHE_LOG_DIR}/error.log + +# +# LogLevel: Control the severity of messages logged to the error_log. +# Available values: trace8, ..., trace1, debug, info, notice, warn, +# error, crit, alert, emerg. +# It is also possible to configure the log level for particular modules, e.g. +# "LogLevel info ssl:warn" +# +LogLevel warn + +# Include module configuration: +IncludeOptional mods-enabled/*.load +IncludeOptional mods-enabled/*.conf + +# Include list of ports to listen on +Include ports.conf + + +# Sets the default security model of the Apache2 HTTPD server. It does +# not allow access to the root filesystem outside of /usr/share and /var/www. +# The former is used by web applications packaged in Debian, +# the latter may be used for local directories served by the web server. If +# your system is serving content from a sub-directory in /srv you must allow +# access here, or in any related virtual host. + + Options FollowSymLinks + AllowOverride None + Require all denied + + + + AllowOverride None + Require all granted + + + + Options Indexes FollowSymLinks + AllowOverride None + Require all granted + + +# AccessFileName: The name of the file to look for in each directory +# for additional configuration directives. See also the AllowOverride +# directive. +# +AccessFileName .htaccess + +# +# The following lines prevent .htaccess and .htpasswd files from being +# viewed by Web clients. +# + + Require all denied + + +# The following directives define some format nicknames for use with +# a CustomLog directive. +# +# These deviate from the Common Log Format definitions in that they use %O +# (the actual bytes sent including headers) instead of %b (the size of the +# requested file), because the latter makes it impossible to detect partial +# requests. +# +# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended. +# Use mod_remoteip instead. +# +LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined +LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined +LogFormat "%h %l %u %t \"%r\" %>s %O" common +LogFormat "%{Referer}i -> %U" referer +LogFormat "%{User-agent}i" agent + +# Include of directories ignores editors' and dpkg's backup files, +# see README.Debian for details. + +# Include generic snippets of statements +IncludeOptional conf-enabled/*.conf + +# Include the virtual host configurations: +IncludeOptional sites-enabled/*.conf + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/bad_conf_file.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/bad_conf_file.conf new file mode 100644 index 000000000..8e9178803 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/bad_conf_file.conf @@ -0,0 +1,3 @@ + + +ServerName invalid.net diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/other-vhosts-access-log.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/other-vhosts-access-log.conf new file mode 100644 index 000000000..5e9f5e9e7 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/other-vhosts-access-log.conf @@ -0,0 +1,4 @@ +# Define an access log for VirtualHosts that don't define their own logfile +CustomLog ${APACHE_LOG_DIR}/other_vhosts_access.log vhost_combined + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/security.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/security.conf new file mode 100644 index 000000000..eccfcb1fd --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/security.conf @@ -0,0 +1,35 @@ +# Changing the following options will not really affect the security of the +# server, but might make attacks slightly more difficult in some cases. + +# +# ServerTokens +# This directive configures what you return as the Server HTTP response +# Header. The default is 'Full' which sends information about the OS-Type +# and compiled in modules. +# Set to one of: Full | OS | Minimal | Minor | Major | Prod +# where Full conveys the most information, and Prod the least. +#ServerTokens Minimal +ServerTokens OS +#ServerTokens Full + +# +# Optionally add a line containing the server version and virtual host +# name to server-generated pages (internal error documents, FTP directory +# listings, mod_status and mod_info output etc., but not CGI generated +# documents or custom error documents). +# Set to "EMail" to also include a mailto: link to the ServerAdmin. +# Set to one of: On | Off | EMail +#ServerSignature Off +ServerSignature On + +# +# Allow TRACE method +# +# Set to "extended" to also reflect the request body (only for testing and +# diagnostic purposes). +# +# Set to one of: On | Off | extended +TraceEnable Off +#TraceEnable On + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/serve-cgi-bin.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/serve-cgi-bin.conf new file mode 100644 index 000000000..b02782dab --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/serve-cgi-bin.conf @@ -0,0 +1,20 @@ + + + Define ENABLE_USR_LIB_CGI_BIN + + + + Define ENABLE_USR_LIB_CGI_BIN + + + + ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ + + AllowOverride None + Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch + Require all granted + + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/other-vhosts-access-log.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/other-vhosts-access-log.conf new file mode 120000 index 000000000..8af91e530 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/other-vhosts-access-log.conf @@ -0,0 +1 @@ +../conf-available/other-vhosts-access-log.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/security.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/security.conf new file mode 120000 index 000000000..036c97fa7 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/security.conf @@ -0,0 +1 @@ +../conf-available/security.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/serve-cgi-bin.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/serve-cgi-bin.conf new file mode 120000 index 000000000..d917f688e --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-enabled/serve-cgi-bin.conf @@ -0,0 +1 @@ +../conf-available/serve-cgi-bin.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/envvars b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/envvars new file mode 100644 index 000000000..a13d9a89e --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/envvars @@ -0,0 +1,29 @@ +# envvars - default environment variables for apache2ctl + +# this won't be correct after changing uid +unset HOME + +# for supporting multiple apache2 instances +if [ "${APACHE_CONFDIR##/etc/apache2-}" != "${APACHE_CONFDIR}" ] ; then + SUFFIX="-${APACHE_CONFDIR##/etc/apache2-}" +else + SUFFIX= +fi + +# Since there is no sane way to get the parsed apache2 config in scripts, some +# settings are defined via environment variables and then used in apache2ctl, +# /etc/init.d/apache2, /etc/logrotate.d/apache2, etc. +export APACHE_RUN_USER=www-data +export APACHE_RUN_GROUP=www-data +# temporary state file location. This might be changed to /run in Wheezy+1 +export APACHE_PID_FILE=/var/run/apache2/apache2$SUFFIX.pid +export APACHE_RUN_DIR=/var/run/apache2$SUFFIX +export APACHE_LOCK_DIR=/var/lock/apache2$SUFFIX +# Only /var/log/apache2 is handled by /etc/logrotate.d/apache2. +export APACHE_LOG_DIR=/var/log/apache2$SUFFIX + +## The locale used by some modules like mod_dav +export LANG=C + +export LANG + diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/authz_svn.load b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/authz_svn.load new file mode 100644 index 000000000..c6df2733b --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/authz_svn.load @@ -0,0 +1,5 @@ +# Depends: dav_svn + + Include mods-enabled/dav_svn.load + +LoadModule authz_svn_module /usr/lib/apache2/modules/mod_authz_svn.so diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav.load b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav.load new file mode 100644 index 000000000..a5867fff3 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav.load @@ -0,0 +1,3 @@ + + LoadModule dav_module /usr/lib/apache2/modules/mod_dav.so + diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav_svn.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav_svn.conf new file mode 100644 index 000000000..801cbd6bd --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav_svn.conf @@ -0,0 +1,56 @@ +# dav_svn.conf - Example Subversion/Apache configuration +# +# For details and further options see the Apache user manual and +# the Subversion book. +# +# NOTE: for a setup with multiple vhosts, you will want to do this +# configuration in /etc/apache2/sites-available/*, not here. + +# ... +# URL controls how the repository appears to the outside world. +# In this example clients access the repository as http://hostname/svn/ +# Note, a literal /svn should NOT exist in your document root. +# + + # Uncomment this to enable the repository + #DAV svn + + # Set this to the path to your repository + #SVNPath /var/lib/svn + # Alternatively, use SVNParentPath if you have multiple repositories under + # under a single directory (/var/lib/svn/repo1, /var/lib/svn/repo2, ...). + # You need either SVNPath and SVNParentPath, but not both. + #SVNParentPath /var/lib/svn + + # Access control is done at 3 levels: (1) Apache authentication, via + # any of several methods. A "Basic Auth" section is commented out + # below. (2) Apache and , also commented out + # below. (3) mod_authz_svn is a svn-specific authorization module + # which offers fine-grained read/write access control for paths + # within a repository. (The first two layers are coarse-grained; you + # can only enable/disable access to an entire repository.) Note that + # mod_authz_svn is noticeably slower than the other two layers, so if + # you don't need the fine-grained control, don't configure it. + + # Basic Authentication is repository-wide. It is not secure unless + # you are using https. See the 'htpasswd' command to create and + # manage the password file - and the documentation for the + # 'auth_basic' and 'authn_file' modules, which you will need for this + # (enable them with 'a2enmod'). + #AuthType Basic + #AuthName "Subversion Repository" + #AuthUserFile /etc/apache2/dav_svn.passwd + + # To enable authorization via mod_authz_svn (enable that module separately): + # + #AuthzSVNAccessFile /etc/apache2/dav_svn.authz + # + + # The following three lines allow anonymous read, but make + # committers authenticate themselves. It requires the 'authz_user' + # module (enable it with 'a2enmod'). + # + #Require valid-user + # + +# diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav_svn.load b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav_svn.load new file mode 100644 index 000000000..e41e1581a --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav_svn.load @@ -0,0 +1,7 @@ +# Depends: dav + + + Include mods-enabled/dav.load + + LoadModule dav_svn_module /usr/lib/apache2/modules/mod_dav_svn.so + diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/rewrite.load b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/rewrite.load new file mode 100644 index 000000000..b32f16264 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/rewrite.load @@ -0,0 +1 @@ +LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/ssl.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/ssl.conf new file mode 100644 index 000000000..e9fcf4f9b --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/ssl.conf @@ -0,0 +1,89 @@ + + + # Pseudo Random Number Generator (PRNG): + # Configure one or more sources to seed the PRNG of the SSL library. + # The seed data should be of good random quality. + # WARNING! On some platforms /dev/random blocks if not enough entropy + # is available. This means you then cannot use the /dev/random device + # because it would lead to very long connection times (as long as + # it requires to make more entropy available). But usually those + # platforms additionally provide a /dev/urandom device which doesn't + # block. So, if available, use this one instead. Read the mod_ssl User + # Manual for more details. + # + SSLRandomSeed startup builtin + SSLRandomSeed startup file:/dev/urandom 512 + SSLRandomSeed connect builtin + SSLRandomSeed connect file:/dev/urandom 512 + + ## + ## SSL Global Context + ## + ## All SSL configuration in this context applies both to + ## the main server and all SSL-enabled virtual hosts. + ## + + # + # Some MIME-types for downloading Certificates and CRLs + # + AddType application/x-x509-ca-cert .crt + AddType application/x-pkcs7-crl .crl + + # Pass Phrase Dialog: + # Configure the pass phrase gathering process. + # The filtering dialog program (`builtin' is a internal + # terminal dialog) has to provide the pass phrase on stdout. + SSLPassPhraseDialog exec:/usr/share/apache2/ask-for-passphrase + + # Inter-Process Session Cache: + # Configure the SSL Session Cache: First the mechanism + # to use and second the expiring timeout (in seconds). + # (The mechanism dbm has known memory leaks and should not be used). + #SSLSessionCache dbm:${APACHE_RUN_DIR}/ssl_scache + SSLSessionCache shmcb:${APACHE_RUN_DIR}/ssl_scache(512000) + SSLSessionCacheTimeout 300 + + # Semaphore: + # Configure the path to the mutual exclusion semaphore the + # SSL engine uses internally for inter-process synchronization. + # (Disabled by default, the global Mutex directive consolidates by default + # this) + #Mutex file:${APACHE_LOCK_DIR}/ssl_mutex ssl-cache + + + # SSL Cipher Suite: + # List the ciphers that the client is permitted to negotiate. See the + # ciphers(1) man page from the openssl package for list of all available + # options. + # Enable only secure ciphers: + SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5 + + # Speed-optimized SSL Cipher configuration: + # If speed is your main concern (on busy HTTPS servers e.g.), + # you might want to force clients to specific, performance + # optimized ciphers. In this case, prepend those ciphers + # to the SSLCipherSuite list, and enable SSLHonorCipherOrder. + # Caveat: by giving precedence to RC4-SHA and AES128-SHA + # (as in the example below), most connections will no longer + # have perfect forward secrecy - if the server's key is + # compromised, captures of past or future traffic must be + # considered compromised, too. + #SSLCipherSuite RC4-SHA:AES128-SHA:HIGH:MEDIUM:!aNULL:!MD5 + #SSLHonorCipherOrder on + + # The protocols to enable. + # Available values: all, SSLv3, TLSv1, TLSv1.1, TLSv1.2 + # SSL v2 is no longer supported + SSLProtocol all + + # Allow insecure renegotiation with clients which do not yet support the + # secure renegotiation protocol. Default: Off + #SSLInsecureRenegotiation on + + # Whether to forbid non-SNI clients to access name based virtual hosts. + # Default: Off + #SSLStrictSNIVHostCheck On + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/ssl.load b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/ssl.load new file mode 100644 index 000000000..3d2336ae0 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/ssl.load @@ -0,0 +1,2 @@ +# Depends: setenvif mime socache_shmcb +LoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/.gitignore b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/authz_svn.load b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/authz_svn.load new file mode 120000 index 000000000..7ac0725dd --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/authz_svn.load @@ -0,0 +1 @@ +../mods-available/authz_svn.load \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav.load b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav.load new file mode 120000 index 000000000..9dcfef6da --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav.load @@ -0,0 +1 @@ +../mods-available/dav.load \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav_svn.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav_svn.conf new file mode 120000 index 000000000..964c7bb0b --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav_svn.conf @@ -0,0 +1 @@ +../mods-available/dav_svn.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav_svn.load b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav_svn.load new file mode 120000 index 000000000..4094e4173 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-enabled/dav_svn.load @@ -0,0 +1 @@ +../mods-available/dav_svn.load \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/ports.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/ports.conf new file mode 100644 index 000000000..5daec58c1 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/ports.conf @@ -0,0 +1,15 @@ +# If you just change the port or add more ports here, you will likely also +# have to change the VirtualHost statement in +# /etc/apache2/sites-enabled/000-default.conf + +Listen 80 + + + Listen 443 + + + + Listen 443 + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/000-default.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/000-default.conf new file mode 100644 index 000000000..2bd4e1fe9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/000-default.conf @@ -0,0 +1,12 @@ + + + ServerName ip-172-30-0-17 + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/certbot.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/certbot.conf new file mode 100644 index 000000000..b3147a523 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/certbot.conf @@ -0,0 +1,42 @@ + +ServerName certbot.demo +ServerAdmin webmaster@localhost + +DocumentRoot /var/www-certbot-reworld/static/ + +Options FollowSymLinks +AllowOverride None + + +Options Indexes FollowSymLinks MultiViews +AllowOverride None +Order allow,deny +allow from all + + +ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ + +AllowOverride None +Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch +Order allow,deny +Allow from all + + +ErrorLog ${APACHE_LOG_DIR}/error.log + +# Possible values include: debug, info, notice, warn, error, crit, +# alert, emerg. +LogLevel warn + +CustomLog ${APACHE_LOG_DIR}/access.log combined + +Alias /doc/ "/usr/share/doc/" + +Options Indexes MultiViews FollowSymLinks +AllowOverride None +Order deny,allow +Deny from all +Allow from 127.0.0.0/255.0.0.0 ::1/128 + + + diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl-port-only.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl-port-only.conf new file mode 100644 index 000000000..849b42e9f --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl-port-only.conf @@ -0,0 +1,36 @@ + + + ServerAdmin webmaster@localhost + + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + # A self-signed (snakeoil) certificate can be created by installing + # the ssl-cert package. See + # /usr/share/doc/apache2/README.Debian.gz for more info. + # If both key and certificate are stored in the same file, only the + # SSLCertificateFile directive is needed. + SSLCertificateFile /etc/apache2/certs/certbot-cert_5.pem + SSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem + + + #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire + + SSLOptions +StdEnvVars + + + SSLOptions +StdEnvVars + + + BrowserMatch "MSIE [2-6]" \ + nokeepalive ssl-unclean-shutdown \ + downgrade-1.0 force-response-1.0 + # MSIE 7 and newer should be able to use keepalive + BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown + + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl.conf new file mode 100644 index 000000000..a3025ae8a --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl.conf @@ -0,0 +1,40 @@ + + + ServerAdmin webmaster@localhost + + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + # SSL Engine Switch: + # Enable/Disable SSL for this virtual host. + SSLEngine on + + # A self-signed (snakeoil) certificate can be created by installing + # the ssl-cert package. See + # /usr/share/doc/apache2/README.Debian.gz for more info. + # If both key and certificate are stored in the same file, only the + # SSLCertificateFile directive is needed. + SSLCertificateFile /etc/apache2/certs/certbot-cert_5.pem + SSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem + + + #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire + + SSLOptions +StdEnvVars + + + SSLOptions +StdEnvVars + + + BrowserMatch "MSIE [2-6]" \ + nokeepalive ssl-unclean-shutdown \ + downgrade-1.0 force-response-1.0 + # MSIE 7 and newer should be able to use keepalive + BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown + + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/encryption-example.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/encryption-example.conf new file mode 100644 index 000000000..4786bda13 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/encryption-example.conf @@ -0,0 +1,42 @@ + + ServerName encryption-example.demo + ServerAdmin webmaster@localhost + + DocumentRoot /var/www-encryption-example/static/ + + Options FollowSymLinks + AllowOverride None + + + Options Indexes FollowSymLinks MultiViews + AllowOverride None + Order allow,deny + allow from all + + + ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ + + AllowOverride None + Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch + Order allow,deny + Allow from all + + + ErrorLog ${APACHE_LOG_DIR}/error.log + + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel warn + + CustomLog ${APACHE_LOG_DIR}/access.log combined + + Alias /doc/ "/usr/share/doc/" + + Options Indexes MultiViews FollowSymLinks + AllowOverride None + Order deny,allow + Deny from all + Allow from 127.0.0.0/255.0.0.0 ::1/128 + + + diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/mod_macro-example.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/mod_macro-example.conf new file mode 100644 index 000000000..6a6579007 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/mod_macro-example.conf @@ -0,0 +1,15 @@ + + + ServerName $domain + ServerAlias www.$domain + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + +Use VHost macro1 test.com +Use VHost macro2 hostname.org +Use VHost macro3 apache.org + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/ocsp-ssl.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/ocsp-ssl.conf new file mode 100644 index 000000000..631cf16c8 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/ocsp-ssl.conf @@ -0,0 +1,36 @@ + +SSLStaplingCache shmcb:/var/run/apache2/stapling_cache(128000) + + # The ServerName directive sets the request scheme, hostname and port that + # the server uses to identify itself. This is used when creating + # redirection URLs. In the context of virtual hosts, the ServerName + # specifies what hostname must appear in the request's Host: header to + # match this virtual host. For the default virtual host (this file) this + # value is not decisive as it is used as a last resort host regardless. + # However, you must set it for any further virtual host explicitly. + ServerName ocspvhost.com + + ServerAdmin webmaster@dumpbits.com + DocumentRoot /var/www/html + + # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, + # error, crit, alert, emerg. + # It is also possible to configure the loglevel for particular + # modules, e.g. + #LogLevel info ssl:warn + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + # For most configuration files from conf-available/, which are + # enabled or disabled at a global level, it is possible to + # include a line for only one particular virtual host. For example the + # following line enables the CGI configuration for this host only + # after it has been globally disabled with "a2disconf". + #Include conf-available/serve-cgi-bin.conf +SSLCertificateFile /etc/apache2/certs/certbot-cert_5.pem +SSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem +SSLUseStapling on + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet + diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/old,default.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/old,default.conf new file mode 100644 index 000000000..2bd4e1fe9 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/old,default.conf @@ -0,0 +1,12 @@ + + + ServerName ip-172-30-0-17 + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf new file mode 100644 index 000000000..33e30a63b --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf @@ -0,0 +1,13 @@ + + + ServerName ip-172-30-0-17 + ServerAdmin webmaster@localhost + DocumentRoot /var/www/html + ServerAlias *.blue.purple.com + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/000-default.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/000-default.conf new file mode 120000 index 000000000..3c4632b73 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/000-default.conf @@ -0,0 +1 @@ +../sites-available/000-default.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/certbot.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/certbot.conf new file mode 120000 index 000000000..4d08c763f --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/certbot.conf @@ -0,0 +1 @@ +../sites-available/certbot.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/encryption-example.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/encryption-example.conf new file mode 120000 index 000000000..417818069 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/encryption-example.conf @@ -0,0 +1 @@ +../sites-available/encryption-example.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/mod_macro-example.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/mod_macro-example.conf new file mode 120000 index 000000000..44f254304 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/mod_macro-example.conf @@ -0,0 +1 @@ +../sites-available/mod_macro-example.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/ocsp-ssl.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/ocsp-ssl.conf new file mode 120000 index 000000000..b25ee0482 --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/ocsp-ssl.conf @@ -0,0 +1 @@ +../sites-available/ocsp-ssl.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/sites b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/sites new file mode 100644 index 000000000..ab518ee5b --- /dev/null +++ b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/sites @@ -0,0 +1,3 @@ +sites-available/certbot.conf, certbot.demo +sites-available/encryption-example.conf, encryption-example.demo +sites-available/ocsp-ssl.conf, ocspvhost.com From a9679e2c25f81e3263a476978c931ce4431797c9 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Tue, 28 Jun 2016 18:08:38 -0700 Subject: [PATCH 217/445] create regression test --- .../certbot_apache/tests/configurator_test.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index ae56002b8..0d3e7e08f 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -1196,29 +1196,24 @@ class AugeasVhostsTest(util.ApacheTest): self.config = util.get_apache_configurator( self.config_path, self.vhost_path, self.config_dir, self.work_dir) - self.config = self.mock_deploy_cert(self.config) self.vh_truth = util.get_vh_truth( self.temp_dir, "debian_apache_2_4/augeas_vhosts") - def mock_deploy_cert(self, config): - """A test for a mock deploy cert""" - self.config.real_deploy_cert = self.config.deploy_cert - - def mocked_deploy_cert(*args, **kwargs): - """a helper to mock a deployed cert""" - with mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod"): - config.real_deploy_cert(*args, **kwargs) - self.config.deploy_cert = mocked_deploy_cert - return self.config - def tearDown(self): shutil.rmtree(self.temp_dir) shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) def test_choosevhost_with_illegal_name(self): + self.config.aug = mock.MagicMock() + self.config.aug.match.side_effect = RuntimeError chosen_vhost = self.config._create_vhost("debian_apache_2_4/augeas_vhosts/apache2/sites-available/old,default.conf") self.assertEqual(None, chosen_vhost) + def test_choosevhost_works(self): + path = "debian_apache_2_4/augeas_vhosts/apache2/sites-available/old,default.conf" + chosen_vhost = self.config._create_vhost(path) + self.assertTrue(chosen_vhost == None or chosen_vhost.path == path) + if __name__ == "__main__": unittest.main() # pragma: no cover From 395843f3f409ae654766d7ef18243b957ea7de87 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 29 Jun 2016 11:06:18 -0700 Subject: [PATCH 218/445] fix coverage --- .../certbot_apache/tests/configurator_test.py | 6 +++ .../apache2/sites-available/000-default.conf | 12 ------ .../apache2/sites-available/certbot.conf | 42 ------------------- .../default-ssl-port-only.conf | 36 ---------------- .../apache2/sites-available/default-ssl.conf | 40 ------------------ .../sites-available/encryption-example.conf | 42 ------------------- .../sites-available/mod_macro-example.conf | 15 ------- .../apache2/sites-available/ocsp-ssl.conf | 36 ---------------- .../apache2/sites-available/wildcard.conf | 13 ------ .../apache2/sites-available/old,default.conf | 12 ------ 10 files changed, 6 insertions(+), 248 deletions(-) delete mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/000-default.conf delete mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/certbot.conf delete mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl-port-only.conf delete mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl.conf delete mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/encryption-example.conf delete mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/mod_macro-example.conf delete mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/ocsp-ssl.conf delete mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf delete mode 100644 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/old,default.conf diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 0d3e7e08f..5de979834 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -1215,5 +1215,11 @@ class AugeasVhostsTest(util.ApacheTest): chosen_vhost = self.config._create_vhost(path) self.assertTrue(chosen_vhost == None or chosen_vhost.path == path) + @mock.patch("certbot_apache.configurator.ApacheConfigurator._create_vhost") + def test_get_vhost_continue(self, mock_vhost): + mock_vhost.return_value = None + vhs = self.config.get_virtual_hosts() + self.assertEqual([], vhs) + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/000-default.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/000-default.conf deleted file mode 100644 index 2bd4e1fe9..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/000-default.conf +++ /dev/null @@ -1,12 +0,0 @@ - - - ServerName ip-172-30-0-17 - ServerAdmin webmaster@localhost - DocumentRoot /var/www/html - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - - -# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/certbot.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/certbot.conf deleted file mode 100644 index b3147a523..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/certbot.conf +++ /dev/null @@ -1,42 +0,0 @@ - -ServerName certbot.demo -ServerAdmin webmaster@localhost - -DocumentRoot /var/www-certbot-reworld/static/ - -Options FollowSymLinks -AllowOverride None - - -Options Indexes FollowSymLinks MultiViews -AllowOverride None -Order allow,deny -allow from all - - -ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ - -AllowOverride None -Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch -Order allow,deny -Allow from all - - -ErrorLog ${APACHE_LOG_DIR}/error.log - -# Possible values include: debug, info, notice, warn, error, crit, -# alert, emerg. -LogLevel warn - -CustomLog ${APACHE_LOG_DIR}/access.log combined - -Alias /doc/ "/usr/share/doc/" - -Options Indexes MultiViews FollowSymLinks -AllowOverride None -Order deny,allow -Deny from all -Allow from 127.0.0.0/255.0.0.0 ::1/128 - - - diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl-port-only.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl-port-only.conf deleted file mode 100644 index 849b42e9f..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl-port-only.conf +++ /dev/null @@ -1,36 +0,0 @@ - - - ServerAdmin webmaster@localhost - - DocumentRoot /var/www/html - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - # A self-signed (snakeoil) certificate can be created by installing - # the ssl-cert package. See - # /usr/share/doc/apache2/README.Debian.gz for more info. - # If both key and certificate are stored in the same file, only the - # SSLCertificateFile directive is needed. - SSLCertificateFile /etc/apache2/certs/certbot-cert_5.pem - SSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem - - - #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire - - SSLOptions +StdEnvVars - - - SSLOptions +StdEnvVars - - - BrowserMatch "MSIE [2-6]" \ - nokeepalive ssl-unclean-shutdown \ - downgrade-1.0 force-response-1.0 - # MSIE 7 and newer should be able to use keepalive - BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown - - - - -# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl.conf deleted file mode 100644 index a3025ae8a..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/default-ssl.conf +++ /dev/null @@ -1,40 +0,0 @@ - - - ServerAdmin webmaster@localhost - - DocumentRoot /var/www/html - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - # SSL Engine Switch: - # Enable/Disable SSL for this virtual host. - SSLEngine on - - # A self-signed (snakeoil) certificate can be created by installing - # the ssl-cert package. See - # /usr/share/doc/apache2/README.Debian.gz for more info. - # If both key and certificate are stored in the same file, only the - # SSLCertificateFile directive is needed. - SSLCertificateFile /etc/apache2/certs/certbot-cert_5.pem - SSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem - - - #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire - - SSLOptions +StdEnvVars - - - SSLOptions +StdEnvVars - - - BrowserMatch "MSIE [2-6]" \ - nokeepalive ssl-unclean-shutdown \ - downgrade-1.0 force-response-1.0 - # MSIE 7 and newer should be able to use keepalive - BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown - - - - -# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/encryption-example.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/encryption-example.conf deleted file mode 100644 index 4786bda13..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/encryption-example.conf +++ /dev/null @@ -1,42 +0,0 @@ - - ServerName encryption-example.demo - ServerAdmin webmaster@localhost - - DocumentRoot /var/www-encryption-example/static/ - - Options FollowSymLinks - AllowOverride None - - - Options Indexes FollowSymLinks MultiViews - AllowOverride None - Order allow,deny - allow from all - - - ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ - - AllowOverride None - Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch - Order allow,deny - Allow from all - - - ErrorLog ${APACHE_LOG_DIR}/error.log - - # Possible values include: debug, info, notice, warn, error, crit, - # alert, emerg. - LogLevel warn - - CustomLog ${APACHE_LOG_DIR}/access.log combined - - Alias /doc/ "/usr/share/doc/" - - Options Indexes MultiViews FollowSymLinks - AllowOverride None - Order deny,allow - Deny from all - Allow from 127.0.0.0/255.0.0.0 ::1/128 - - - diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/mod_macro-example.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/mod_macro-example.conf deleted file mode 100644 index 6a6579007..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/mod_macro-example.conf +++ /dev/null @@ -1,15 +0,0 @@ - - - ServerName $domain - ServerAlias www.$domain - DocumentRoot /var/www/html - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - -Use VHost macro1 test.com -Use VHost macro2 hostname.org -Use VHost macro3 apache.org - -# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/ocsp-ssl.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/ocsp-ssl.conf deleted file mode 100644 index 631cf16c8..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/ocsp-ssl.conf +++ /dev/null @@ -1,36 +0,0 @@ - -SSLStaplingCache shmcb:/var/run/apache2/stapling_cache(128000) - - # The ServerName directive sets the request scheme, hostname and port that - # the server uses to identify itself. This is used when creating - # redirection URLs. In the context of virtual hosts, the ServerName - # specifies what hostname must appear in the request's Host: header to - # match this virtual host. For the default virtual host (this file) this - # value is not decisive as it is used as a last resort host regardless. - # However, you must set it for any further virtual host explicitly. - ServerName ocspvhost.com - - ServerAdmin webmaster@dumpbits.com - DocumentRoot /var/www/html - - # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, - # error, crit, alert, emerg. - # It is also possible to configure the loglevel for particular - # modules, e.g. - #LogLevel info ssl:warn - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - # For most configuration files from conf-available/, which are - # enabled or disabled at a global level, it is possible to - # include a line for only one particular virtual host. For example the - # following line enables the CGI configuration for this host only - # after it has been globally disabled with "a2disconf". - #Include conf-available/serve-cgi-bin.conf -SSLCertificateFile /etc/apache2/certs/certbot-cert_5.pem -SSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem -SSLUseStapling on - -# vim: syntax=apache ts=4 sw=4 sts=4 sr noet - diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf deleted file mode 100644 index 33e30a63b..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf +++ /dev/null @@ -1,13 +0,0 @@ - - - ServerName ip-172-30-0-17 - ServerAdmin webmaster@localhost - DocumentRoot /var/www/html - ServerAlias *.blue.purple.com - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - - -# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/old,default.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/old,default.conf deleted file mode 100644 index 2bd4e1fe9..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/old,default.conf +++ /dev/null @@ -1,12 +0,0 @@ - - - ServerName ip-172-30-0-17 - ServerAdmin webmaster@localhost - DocumentRoot /var/www/html - - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - - -# vim: syntax=apache ts=4 sw=4 sts=4 sr noet From b64da855a2bf0c482ddff53ef508b7bba1bbdb30 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Wed, 29 Jun 2016 11:55:22 -0700 Subject: [PATCH 219/445] lint --- .../certbot_apache/tests/configurator_test.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 5de979834..a90940f94 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -1,4 +1,4 @@ -# pylint: disable=too-many-public-methods +# pylint: disable=too-many-public-methods, protected-access """Test for certbot_apache.configurator.""" import os import shutil @@ -1190,9 +1190,12 @@ class AugeasVhostsTest(util.ApacheTest): """Test vhosts with illegal names dependant on augeas version.""" def setUp(self): # pylint: disable=arguments-differ - super(AugeasVhostsTest, self).setUp(test_dir="debian_apache_2_4/augeas_vhosts", - config_root="debian_apache_2_4/augeas_vhosts/apache2", - vhost_root="debian_apache_2_4/augeas_vhosts/apache2/sites-available") + td = "debian_apache_2_4/augeas_vhosts" + cr = "debian_apache_2_4/augeas_vhosts/apache2" + vr = "debian_apache_2_4/augeas_vhosts/apache2/sites-available" + super(AugeasVhostsTest, self).setUp(test_dir=td, + config_root=cr, + vhost_root=vr) self.config = util.get_apache_configurator( self.config_path, self.vhost_path, self.config_dir, self.work_dir) @@ -1207,7 +1210,8 @@ class AugeasVhostsTest(util.ApacheTest): def test_choosevhost_with_illegal_name(self): self.config.aug = mock.MagicMock() self.config.aug.match.side_effect = RuntimeError - chosen_vhost = self.config._create_vhost("debian_apache_2_4/augeas_vhosts/apache2/sites-available/old,default.conf") + path = "debian_apache_2_4/augeas_vhosts/apache2/sites-available/old,default.conf" + chosen_vhost = self.config._create_vhost(path) self.assertEqual(None, chosen_vhost) def test_choosevhost_works(self): From 7b50960ac0559e4f2e9fea20a72470127b264515 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 29 Jun 2016 13:57:58 -0700 Subject: [PATCH 220/445] Address review comments --- certbot-apache/certbot_apache/configurator.py | 2 +- certbot-apache/certbot_apache/obj.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index c51778ce5..89d602f5f 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -328,7 +328,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if vhost is None: logger.error( "No vhost exists with servername or alias of: %s " - "(or it's in a file with multiple files, which Certbot " + "(or it's in a file with multiple vhosts, which Certbot " "can't parse yet). " "No vhost was selected. Please specify ServerName or ServerAlias " "in the Apache config, or split vhosts into separate files.", diff --git a/certbot-apache/certbot_apache/obj.py b/certbot-apache/certbot_apache/obj.py index c1af352cb..c71443e92 100644 --- a/certbot-apache/certbot_apache/obj.py +++ b/certbot-apache/certbot_apache/obj.py @@ -19,12 +19,12 @@ class Addr(common.Addr): self.is_wildcard() and other.is_wildcard())) return False - def __repr__(self): - return "certbot_apache.obj.Addr(" + repr(self.tup) + ")" - def __ne__(self, other): return not self.__eq__(other) + def __repr__(self): + return "certbot_apache.obj.Addr(" + repr(self.tup) + ")" + def _addr_less_specific(self, addr): """Returns if addr.get_addr() is more specific than self.get_addr().""" # pylint: disable=protected-access From a70cf6fcf80c3c3322033d0f69f0802df91a4c9f Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 29 Jun 2016 15:22:19 -0700 Subject: [PATCH 221/445] Use correct port name --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 4e49c3eb5..fb96bb853 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -428,7 +428,7 @@ Operating System Packages **FreeBSD** - * Port: ``cd /usr/ports/security/py-letsencrypt && make install clean`` + * Port: ``cd /usr/ports/security/py-certbot && make install clean`` * Package: ``pkg install py27-letsencrypt`` **OpenBSD** From 92870f0bbb2a912b16f77079243efeb4e5293bef Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Thu, 30 Jun 2016 10:22:12 -0500 Subject: [PATCH 222/445] Better docstring for _signal_handler --- certbot/error_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index 5b9220583..42861b4a1 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -112,7 +112,8 @@ class ErrorHandler(object): self.prev_handlers.clear() def _signal_handler(self, signum, unused_frame): - """Stores the recieved signal. + """Stores the recieved signal. If we are executing the code block in + the body of the context manager, stop by raising signal exit. :param int signum: number of current signal From 8c3e443de9d26d0e722956107db8a36134c0134c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 30 Jun 2016 15:07:28 -0700 Subject: [PATCH 223/445] First attempt at mitigating #3206 --- certbot-nginx/certbot_nginx/nginxparser.py | 31 +++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index d6c352296..895c4f8a3 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -38,19 +38,42 @@ class RawNginxParser(object): assignment = space + key + Optional(space + value, default=None) + semicolon location_statement = space + Optional(modifier) + Optional(space + location + space) if_statement = space + Literal("if") + space + condition + space + map_statement = space + Literal("map") + space + nonspace + space + dollar_var + space + + # This is NOT an accurate way to parse nginx map entries; it's almost + # certianly too permissive and may be wrong in other ways, but it should + # preserve things correctly in mmmmost or all cases. + # - it sometimes splits the two tokens incorrectly eg + # '''"~Opera Mini" 1''' -> ['"~Opera', ' Mini" 1'] + # - I can neither prove nor disprove that it is corect wrt all escaped + # semicolon situations + # Addresses https://github.com/fatiherikli/nginxparser/issues/19 + + map_entry = space + nonspace + value + space + semicolon + map_block = Forward() + map_block << Group( + # key could for instance be "server" or "http", or "location" (in which case + # location_statement needs to have a non-empty location) + Group(map_statement).leaveWhitespace() + + left_bracket + + Group(ZeroOrMore(Group(comment | map_entry)) + space).leaveWhitespace() + + right_bracket) + + block = Forward() block << Group( # key could for instance be "server" or "http", or "location" (in which case # location_statement needs to have a non-empty location) - (Group(space + key + location_statement) ^ Group(if_statement) ^ - Group(map_statement)).leaveWhitespace() + + (Group(space + key + location_statement) ^ Group(if_statement)).leaveWhitespace() + left_bracket + - Group(ZeroOrMore(Group(comment | assignment) | block) + space).leaveWhitespace() + + Group(ZeroOrMore(Group(comment | assignment) | block | map_block) + space).leaveWhitespace() + right_bracket) - script = OneOrMore(Group(comment | assignment) ^ block) + space + stringEnd + + + script = OneOrMore(Group(comment | assignment) ^ block ^ map_block) + space + stringEnd script.parseWithTabs() def __init__(self, source): From db8ddac4e252780fb4015298b7cbe0388d340f10 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 30 Jun 2016 15:13:35 -0700 Subject: [PATCH 224/445] lint & tweak --- certbot-nginx/certbot_nginx/nginxparser.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 895c4f8a3..50ed41eb9 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -40,7 +40,6 @@ class RawNginxParser(object): if_statement = space + Literal("if") + space + condition + space map_statement = space + Literal("map") + space + nonspace + space + dollar_var + space - # This is NOT an accurate way to parse nginx map entries; it's almost # certianly too permissive and may be wrong in other ways, but it should # preserve things correctly in mmmmost or all cases. @@ -49,7 +48,6 @@ class RawNginxParser(object): # - I can neither prove nor disprove that it is corect wrt all escaped # semicolon situations # Addresses https://github.com/fatiherikli/nginxparser/issues/19 - map_entry = space + nonspace + value + space + semicolon map_block = Forward() map_block << Group( @@ -60,18 +58,14 @@ class RawNginxParser(object): Group(ZeroOrMore(Group(comment | map_entry)) + space).leaveWhitespace() + right_bracket) - block = Forward() - block << Group( # key could for instance be "server" or "http", or "location" (in which case # location_statement needs to have a non-empty location) (Group(space + key + location_statement) ^ Group(if_statement)).leaveWhitespace() + left_bracket + - Group(ZeroOrMore(Group(comment | assignment) | block | map_block) + space).leaveWhitespace() + - right_bracket) - - + Group(ZeroOrMore(Group(comment | assignment) | block | map_block) + space).leaveWhitespace() + + right_bracket) script = OneOrMore(Group(comment | assignment) ^ block ^ map_block) + space + stringEnd script.parseWithTabs() From be8f0bc53b657fdf3a104c355858c396f96eb6a0 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 30 Jun 2016 15:29:38 -0700 Subject: [PATCH 225/445] Do a better job of parsing map patterns --- certbot-nginx/certbot_nginx/nginxparser.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 50ed41eb9..0cc912515 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -27,6 +27,8 @@ class RawNginxParser(object): condition = Regex(r"\(.+\)") # Matches anything that is not a special character AND any chars in single # or double quotes + # All of these COULD be upgraded to something like + # https://stackoverflow.com/a/16130746 value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") location = CharsNotIn("{};," + string.whitespace) # modifier for location uri [ = | ~ | ~* | ^~ ] @@ -43,14 +45,13 @@ class RawNginxParser(object): # This is NOT an accurate way to parse nginx map entries; it's almost # certianly too permissive and may be wrong in other ways, but it should # preserve things correctly in mmmmost or all cases. - # - it sometimes splits the two tokens incorrectly eg - # '''"~Opera Mini" 1''' -> ['"~Opera', ' Mini" 1'] + # # - I can neither prove nor disprove that it is corect wrt all escaped # semicolon situations # Addresses https://github.com/fatiherikli/nginxparser/issues/19 - map_entry = space + nonspace + value + space + semicolon - map_block = Forward() - map_block << Group( + map_pattern = Regex(r'".*"') | Regex(r"'.*'") | nonspace + map_entry = space + map_pattern + space + value + space + semicolon + map_block = Group( # key could for instance be "server" or "http", or "location" (in which case # location_statement needs to have a non-empty location) Group(map_statement).leaveWhitespace() + From f3915705668f1da08077c2f5e42b317b14df462b Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 30 Jun 2016 19:01:02 -0700 Subject: [PATCH 226/445] seth and noah updated some confusing things --- README.rst | 11 +++-- docs/using.rst | 119 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 97 insertions(+), 33 deletions(-) diff --git a/README.rst b/README.rst index c71079f9a..59be650dd 100644 --- a/README.rst +++ b/README.rst @@ -41,7 +41,7 @@ certbot.eff.org_ to find out), you can install it from there, and run it by typing ``certbot`` (or ``letsencrypt``). Because not all operating systems have packages yet, we provide a temporary solution via the ``certbot-auto`` wrapper script, which obtains some dependencies from -your OS and puts others in a python virtual environment:: +your OS and puts others in a Python virtual environment:: user@webserver:~$ wget https://dl.eff.org/certbot-auto user@webserver:~$ chmod a+x ./certbot-auto @@ -60,9 +60,12 @@ And for full command line help, you can type:: ``certbot-auto`` updates to the latest client release automatically. And since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly -the same command line flags and arguments. More details about this script and -other installation methods can be found `in the User Guide -`_. +the same command line flags and arguments. Throughout our and others' documentation +you should substitute the name of the command that certbot.eff.org_ told you +to use on your system. (``certbot-auto`` should always be run from the directory +where it has been downloaded and invoked via ``./certbot-auto``). +More details about this script and other installation methods can be found +`in the User Guide `_. How to run the client --------------------- diff --git a/docs/using.rst b/docs/using.rst index fb96bb853..bb6741cc4 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -9,34 +9,46 @@ Getting Certbot =============== To get specific instructions for installing Certbot on your OS, we recommend -visiting certbot.eff.org_. If you're offline, you can find some general -instructions `in the README / Introduction `__ +visiting certbot.eff.org_. For general information on how to install Certbot, +and the difference between the ``certbot``, ``certbot-auto``, ``letsencrypt``, +and ``letsencrypt-auto`` commands, please refer to the +`README / Introduction `__ __ installation_ .. _certbot.eff.org: https://certbot.eff.org .. _certbot-auto: -The name of the certbot command -------------------------------- - -Many platforms now have native packages that give you a ``certbot`` or (for -older packages) ``letsencrypt`` command you can run. On others, the -``certbot-auto`` / ``letsencrypt-auto`` installer and wrapper script is a -stand-in. Throughout the documentation, whenever you see references to -``certbot`` script/binary, you should substitute in the name of the command -that certbot.eff.org_ told you to use on your system (``certbot``, -``letsencrypt``, or ``certbot-auto``). +Commands +======== +The Certbot client uses a number of different "commands" (also referred +to, equivalently, as "subcommands") to request specific actions such as +obtaining, renewing, or revoking certificates. Some of the most important +and most commonly-used commands will be discussed throughout this +document; an exhaustive list also appears near the end of the document. Plugins ======= The Certbot client supports a number of different "plugins" that can be -used to obtain and/or install certificates. Plugins that can obtain a cert -are called "authenticators" and can be used with the "certonly" command. -Plugins that can install a cert are called "installers". Plugins that do both -can be used with the "certbot run" command, which is the default. +used to obtain and/or install certificates. + +Plugins that can obtain a cert are called "authenticators" and can be used with +the "certonly" command. This will carry out the steps needed to validate that you +control the domain(s) you are requesting a cert for, obtain a cert for the specified +domain(s), and place it in the ``/etc/letsencrypt`` directory on your +machine - without editing any of your server's configuration files to serve the +obtained certificate. If you specify multiple domains to authenticate, they will +all be listed in a single certificate. To obtain multiple seperate certificates +you will need to run Certbot multiple times. + +Plugins that can install a cert are called "installers" and can be used with the +"install" command. These plugins can modify your webserver's configuration to +serve your website over HTTPS using certificates obtained by certbot. + +Plugins that do both can be used with the "certbot run" command, which is the default +when no command is specified. =========== ==== ==== =============================================================== Plugin Auth Inst Notes @@ -54,7 +66,8 @@ manual_ Y N Helps you obtain a cert by giving you instructions to perf nginx_ Y Y Very experimental and not included in certbot-auto_. =========== ==== ==== =============================================================== -There are many third-party-plugins_ available. +There are many third-party-plugins_ available. Below we describe in more detail +the circumstances in which each plugin can be used, and how to use it. Apache ------ @@ -183,7 +196,54 @@ postfix_ N Y STARTTLS Everywhere is becoming a Certbot Postfix/Exim plu If you're interested, you can also :ref:`write your own plugin `. +Re-running Certbot +================== +Running Certbot with the ``certonly`` or ``run`` commands always requests +the creation of a single new certificate, even if you already have an +existing certificate with some of the same domain names. The ``--force-renewal``, +``--duplicate``, and ``--expand`` options control Certbot's behavior in this case. +If you don't specify a requested behavior, Certbot may ask you what you intended. + +``--force-renewal`` tells Certbot to request a new certificate +with the same domains as an existing certificate. (Each and every domain +must be explicitly specified via ``-d``.) If successful, this certificate +will be saved alongside the earlier one and symbolic links (the "``live``" +reference) will be updated to point to the new certificate. This is a +valid method of explicitly requesting the renewal of a specific individual +certificate. + +``--duplicate`` tells Certbot to create a separate, unrelated certificate +with the same domains as an existing certificate. This certificate will +be saved completely separately from the prior one. Most users probably +do not want this behavior. + +``--expand`` tells Certbot to update an existing certificate with a new +certificate that contains all of the old domains and one or more additional +new domains. + +Whenever you obtain a new certificate in any of these ways, the new +certificate exists alongside any previously-obtained certificates, whether +or not the previous certificates have expired. The generation of a new +certificate counts against several rate limits that are intended to prevent +abuse of the ACME protocol, as described +`here `__. + +Certbot also provides a ``renew`` command. This command examines *all* existing +certificates to determine whether or not each is near expiry. For any existing +certificate that is near expiry, ``certbot renew`` will attempt to obtain a +new certificate for the same domains. Unlike ``certonly``, ``renew`` acts on +multiple certificates and always takes into account whether each one is near +expiry. Because of this, ``renew`` is suitable (and designed) for automated use, +to allow your system to automatically renew each certificate when appropriate. +Since ``renew`` will only renew certificates that are near expiry it can be +run as frequently as you want - since it will usually take no action. + +Typically, ``certbot renew`` runs a reduced risk of rate-limit problems +because it renews certificates only when necessary, and because some of +the Let's Encrypt CA's rate limit policies treat the issuance of a new +certificate under these circumstances more generously. More details about +the use of ``certbot renew`` are provided below. Renewal ======= @@ -204,10 +264,11 @@ at the time the certificate was originally issued will be used for the renewal attempt, unless you specify other plugins or options. You can also specify hooks to be run before or after a certificate is -renewed. For example, if you want to use the standalone_ plugin to renew -your certificates, you may want to use a command like +renewed. For example, if you have only a single cert and you obtained it using +the standalone_ plugin, it will be used by default when renewing. In that case +you may want to use a command like this to renew your certificate. -``certbot renew --standalone --pre-hook "service nginx stop" --post-hook "service nginx start"`` +``certbot renew --pre-hook "service nginx stop" --post-hook "service nginx start"`` This will stop Nginx so standalone can bind to the necessary ports and then restart Nginx after the plugin is finished. The hooks will only be @@ -223,12 +284,13 @@ can run on a regular basis, like every week or every day). In that case, you are likely to want to use the ``-q`` or ``--quiet`` quiet flag to silence all output except errors. -The ``--force-renew`` flag may be helpful for automating renewal; -it causes the expiration time of the certificate(s) to be ignored when -considering renewal, and attempts to renew each and every installed -certificate regardless of its age. (This form is not appropriate to run -daily because each certificate will be renewed every day, which will -quickly run into the certificate authority rate limit.) +If you are manually renewing all of your certificates, the +``--force-renewal`` flag may be helpful; it causes the expiration time of +the certificate(s) to be ignored when considering renewal, and attempts to +renew each and every installed certificate regardless of its age. (This +form is not appropriate to run daily because each certificate will be +renewed every day, which will quickly run into the certificate authority +rate limit.) Note that options provided to ``certbot renew`` will apply to *every* certificate for which renewal is attempted; for example, @@ -238,7 +300,6 @@ RSA public key. If a certificate is successfully renewed using specified options, those options will be saved and used for future renewals of that certificate. - An alternative form that provides for more fine-grained control over the renewal process (while renewing specified certificates one at a time), is ``certbot certonly`` with the complete set of subject domains of @@ -253,8 +314,8 @@ this case in order to renew and replace the old certificate rather than obtaining a new one; don't forget any `www.` domains! Specifying a subset of the domains creates a new, separate certificate containing only those domains, rather than replacing the original certificate.) -The ``certonly`` form attempts to renew one individual certificate. - +When run with a set of domains corresponding to an existing certificate, +the ``certonly`` command attempts to renew that one individual certificate. Please note that the CA will send notification emails to the address you provide if you do not renew certificates that are about to expire. From a9abc7b39e89ee26c116244e528d49931f922252 Mon Sep 17 00:00:00 2001 From: sagi Date: Fri, 1 Jul 2016 15:17:37 +0000 Subject: [PATCH 227/445] typo --- certbot-apache/certbot_apache/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 89d602f5f..fdc0f37d8 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -874,7 +874,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if self._sift_line(line): if not sift: new_file.write( - "# Some rewrite rules in this file were " + "# Some rewrite rules in this file " "were disabled on your HTTPS site,\n" "# because they have the potential to " "create redirection loops.\n") From 15ba12ed4647990d8e72f244a682c00327493443 Mon Sep 17 00:00:00 2001 From: sagi Date: Fri, 1 Jul 2016 21:06:16 +0000 Subject: [PATCH 228/445] Parsing State Machine + some tests --- certbot-apache/certbot_apache/configurator.py | 64 ++++++++++++++++--- .../certbot_apache/tests/configurator_test.py | 11 ++-- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index fdc0f37d8..23c7a0c29 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -819,7 +819,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): else: return non_ssl_vh_fp + self.conf("le_vhost_ext") - def _sift_line(self, line): + def _sift_rewrite_rule(self, line): """Decides whether a line should be copied to a SSL vhost. A canonical example of when sifting a line is required: @@ -861,7 +861,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): A new file is created on the filesystem. """ - # First register the creation so that it is properly removed if + # First register the creation so thatu it is properly removed if # configuration is rolled back self.reverter.register_file_creation(False, ssl_fp) sift = False @@ -870,18 +870,62 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): with open(avail_fp, "r") as orig_file: with open(ssl_fp, "w") as new_file: new_file.write("\n") + + comment = ("# Some rewrite rules in this file were " + "disabled on your HTTPS site,\n" + "# because they have the potential to create " + "redirection loops.\n") + for line in orig_file: - if self._sift_line(line): + A = line.lstrip().startswith("RewriteCond") + B = line.lstrip().startswith("RewriteRule") + + if not (A or B): + new_file.write(line) + continue + + # A RewriteRule that doesn't need filtering + if B and not self._sift_rewrite_rule(line): + new_file.write(line) + continue + + # A RewriteRule that does need filtering + if B and self._sift_rewrite_rule(line): if not sift: - new_file.write( - "# Some rewrite rules in this file " - "were disabled on your HTTPS site,\n" - "# because they have the potential to " - "create redirection loops.\n") + new_file.write(comment) sift = True new_file.write("# " + line) - else: - new_file.write(line) + continue + + # We save RewriteCond(s) and their corresponding + # RewriteRule in 'chunk'. + # We then decide whether we comment out the entire + # chunk based on its RewriteRule. + chunk = [] + if A: + chunk.append(line) + line = next(orig_file) + + # RewriteCond(s) must be followed by one RewriteRule + while not line.lstrip().startswith("RewriteRule"): + chunk.append(line) + line = next(orig_file) + + # Now, current line must start with a RewriteRule + chunk.append(line) + + if self._sift_rewrite_rule(line): + if not sift: + new_file.write(comment) + sift = True + + new_file.write(''.join( + ['# ' + l for l in chunk])) + continue + else: + new_file.write(''.join(chunk)) + continue + new_file.write("\n") except IOError: logger.fatal("Error writing/reading to file in make_vhost_ssl") diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 9a034c3e0..5a8684c9a 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -1110,16 +1110,19 @@ class MultipleVhostsTest(util.ApacheTest): self.config._enable_redirect(self.vh_truth[1], "") self.assertEqual(len(self.config.vhosts), 9) - def test_sift_line(self): + def test_sift_rewrite_rule(self): # pylint: disable=protected-access small_quoted_target = "RewriteRule ^ \"http://\"" - self.assertFalse(self.config._sift_line(small_quoted_target)) + self.assertFalse(self.config._sift_rewrite_rule(small_quoted_target)) https_target = "RewriteRule ^ https://satoshi" - self.assertTrue(self.config._sift_line(https_target)) + self.assertTrue(self.config._sift_rewrite_rule(https_target)) normal_target = "RewriteRule ^/(.*) http://www.a.com:1234/$1 [L,R]" - self.assertFalse(self.config._sift_line(normal_target)) + self.assertFalse(self.config._sift_rewrite_rule(normal_target)) + + not_rewriterule = "NotRewriteRule ^ ..." + self.assertFalse(self.config._sift_rewrite_rule(not_rewriterule)) @mock.patch("certbot_apache.configurator.zope.component.getUtility") def test_make_vhost_ssl_with_existing_rewrite_rule(self, mock_get_utility): From 74593607803e67818ab23b0e3f7a772ee99bc417 Mon Sep 17 00:00:00 2001 From: sagi Date: Fri, 1 Jul 2016 22:08:37 +0000 Subject: [PATCH 229/445] Add more test cases --- .../certbot_apache/tests/configurator_test.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 5a8684c9a..57c6a8009 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -1151,7 +1151,61 @@ class MultipleVhostsTest(util.ApacheTest): "[L,QSA,R=permanent]") self.assertTrue(commented_rewrite_rule in conf_text) mock_get_utility().add_message.assert_called_once_with(mock.ANY, + mock.ANY) + @mock.patch("certbot_apache.configurator.zope.component.getUtility") + def test_make_vhost_ssl_with_existing_rewrite_conds(self, mock_get_utility): + self.config.parser.modules.add("rewrite_module") + + http_vhost = self.vh_truth[0] + + self.config.parser.add_dir( + http_vhost.path, "RewriteEngine", "on") + + # Add a chunk that should not be commented out. + self.config.parser.add_dir(http_vhost.path, + "RewriteCond", ["%{DOCUMENT_ROOT}/%{REQUEST_FILENAME}", "!-f"]) + self.config.parser.add_dir( + http_vhost.path, "RewriteRule", + ["^(.*)$", "b://u%{REQUEST_URI}", "[P,QSA,L]"]) + + # Add a chunk that should be commented out. + self.config.parser.add_dir(http_vhost.path, + "RewriteCond", ["%{HTTPS}", "!=on"]) + self.config.parser.add_dir(http_vhost.path, + "RewriteCond", ["%{HTTPS}", "!^$"]) + self.config.parser.add_dir( + http_vhost.path, "RewriteRule", + ["^", + "https://%{SERVER_NAME}%{REQUEST_URI}", + "[L,QSA,R=permanent]"]) + + self.config.save() + + ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0]) + + conf_line_set = set(open(ssl_vhost.filep).read().splitlines()) + + not_commented_cond1 = ("RewriteCond " + "%{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f") + not_commented_rewrite_rule = ("RewriteRule " + "^(.*)$ b://u%{REQUEST_URI} [P,QSA,L]") + + commented_cond1 = "# RewriteCond %{HTTPS} !=on" + commented_cond2 = "# RewriteCond %{HTTPS} !^$" + commented_rewrite_rule = ("# RewriteRule ^ " + "https://%{SERVER_NAME}%{REQUEST_URI} " + "[L,QSA,R=permanent]") + + self.assertTrue(not_commented_cond1 in conf_line_set) + self.assertTrue(not_commented_rewrite_rule in conf_line_set) + + self.assertTrue(commented_cond1 in conf_line_set) + self.assertTrue(commented_cond2 in conf_line_set) + self.assertTrue(commented_rewrite_rule in conf_line_set) + mock_get_utility().add_message.assert_called_once_with(mock.ANY, + mock.ANY) + def get_achalls(self): """Return testing achallenges.""" From 0e9622322a89f8efbeb149d4ccf8cb33ddc19660 Mon Sep 17 00:00:00 2001 From: sagi Date: Fri, 1 Jul 2016 22:17:41 +0000 Subject: [PATCH 230/445] typo --- certbot-apache/certbot_apache/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 23c7a0c29..0a24759dc 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -861,7 +861,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): A new file is created on the filesystem. """ - # First register the creation so thatu it is properly removed if + # First register the creation so that it is properly removed if # configuration is rolled back self.reverter.register_file_creation(False, ssl_fp) sift = False From 2cd4f6f008a9762db08f053084cbabd2d53c7384 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 5 Jul 2016 14:14:31 -0700 Subject: [PATCH 231/445] update FreeBSD package name --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index fb96bb853..806dfb340 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -429,7 +429,7 @@ Operating System Packages **FreeBSD** * Port: ``cd /usr/ports/security/py-certbot && make install clean`` - * Package: ``pkg install py27-letsencrypt`` + * Package: ``pkg install py27-certbot`` **OpenBSD** From 777a654d90ec347231c81c7e6c6135e77f58fce6 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 5 Jul 2016 23:13:16 -0500 Subject: [PATCH 232/445] Move SignalExit to errors.py --- certbot/error_handler.py | 13 +++---------- certbot/errors.py | 4 ++++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index 42861b4a1..45d37f847 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -5,6 +5,7 @@ import os import signal import traceback +from certbot import errors logger = logging.getLogger(__name__) @@ -19,14 +20,6 @@ _SIGNALS = ([signal.SIGTERM] if os.name == "nt" else signal.SIGXCPU, signal.SIGXFSZ]) -class SignalExit(Exception): - """This exception is used stop of execution of the code in the body of the - ErrorHandler context manager. - - """ - pass - - class ErrorHandler(object): """Registers functions to be called if an exception or signal occurs. @@ -62,7 +55,7 @@ class ErrorHandler(object): def __exit__(self, exec_type, exec_value, trace): self.body_executed = True retval = False - if exec_type is SignalExit: + if exec_type is errors.SignalExit: logger.debug("Encountered signals: %s", self.received_signals) self.call_registered() for signum in self.received_signals: @@ -120,7 +113,7 @@ class ErrorHandler(object): """ self.received_signals.append(signum) if not self.body_executed: - raise SignalExit + raise errors.SignalExit def call_signal(self, signum): """Calls the signal given by signum. diff --git a/certbot/errors.py b/certbot/errors.py index 1553b6317..738b7536b 100644 --- a/certbot/errors.py +++ b/certbot/errors.py @@ -29,6 +29,10 @@ class HookCommandNotFound(Error): """Failed to find a hook command in the PATH.""" +class SignalExit(Error): + """A Unix signal was recieved while in the ErrorHandler context manager.""" + + # Auth Handler Errors class AuthorizationError(Error): """Authorization error.""" From 15336d45bd9f16c9acc1c0662e1c03caf40f7b15 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 5 Jul 2016 23:13:45 -0500 Subject: [PATCH 233/445] Reset self.body_executed in __enter__ --- certbot/error_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index 45d37f847..a0f9f9143 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -50,6 +50,7 @@ class ErrorHandler(object): self.register(func, *args, **kwargs) def __enter__(self): + self.body_executed = False self.set_signal_handlers() def __exit__(self, exec_type, exec_value, trace): From 79c602ffc83bfac8f95a5d1e89464dd68c2af16a Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 5 Jul 2016 21:30:22 -0500 Subject: [PATCH 234/445] Make non-public methods as private. --- certbot/error_handler.py | 18 +++++++++--------- certbot/tests/error_handler_test.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index a0f9f9143..10e956e13 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -51,24 +51,24 @@ class ErrorHandler(object): def __enter__(self): self.body_executed = False - self.set_signal_handlers() + self._set_signal_handlers() def __exit__(self, exec_type, exec_value, trace): self.body_executed = True retval = False if exec_type is errors.SignalExit: logger.debug("Encountered signals: %s", self.received_signals) - self.call_registered() + self._call_registered() for signum in self.received_signals: - self.call_signal(signum) + self._call_signal(signum) retval = True # SystemExit is ignored to properly handle forks that don't exec elif exec_type not in (None, SystemExit): logger.debug("Encountered exception:\n%s", "".join( traceback.format_exception(exec_type, exec_value, trace))) - self.call_registered() + self._call_registered() - self.reset_signal_handlers() + self._reset_signal_handlers() return retval def register(self, func, *args, **kwargs): @@ -79,7 +79,7 @@ class ErrorHandler(object): """ self.funcs.append(functools.partial(func, *args, **kwargs)) - def call_registered(self): + def _call_registered(self): """Calls all registered functions""" logger.debug("Calling registered functions") while self.funcs: @@ -90,7 +90,7 @@ class ErrorHandler(object): logger.exception(error) self.funcs.pop() - def set_signal_handlers(self): + def _set_signal_handlers(self): """Sets signal handlers for signals in _SIGNALS.""" for signum in _SIGNALS: prev_handler = signal.getsignal(signum) @@ -99,7 +99,7 @@ class ErrorHandler(object): self.prev_handlers[signum] = prev_handler signal.signal(signum, self._signal_handler) - def reset_signal_handlers(self): + def _reset_signal_handlers(self): """Resets signal handlers for signals in _SIGNALS.""" for signum in self.prev_handlers: signal.signal(signum, self.prev_handlers[signum]) @@ -116,7 +116,7 @@ class ErrorHandler(object): if not self.body_executed: raise errors.SignalExit - def call_signal(self, signum): + def _call_signal(self, signum): """Calls the signal given by signum. :param int signum: signal number diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index f8ac0718b..a7906c5ef 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -68,7 +68,7 @@ class ErrorHandlerTest(unittest.TestCase): def test_bad_recovery(self): bad_func = mock.MagicMock(side_effect=[ValueError]) self.handler.register(bad_func) - self.handler.call_registered() + self.handler._call_registered() self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) bad_func.assert_called_once_with() From 277b7a89f10a5cfa8b216db77d0d522d1fe8fc94 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 5 Jul 2016 21:59:00 -0500 Subject: [PATCH 235/445] Update ErrorHandler docstring. --- certbot/error_handler.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index 10e956e13..f3226de9d 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -21,19 +21,26 @@ _SIGNALS = ([signal.SIGTERM] if os.name == "nt" else class ErrorHandler(object): - """Registers functions to be called if an exception or signal occurs. + """Context manager for running code that must be cleaned up on failure. - This class allows you to register functions that will be called when - an exception (excluding SystemExit) or signal is encountered. The - class works best as a context manager. For example: + The context manager allows you to register functions that will be called + when an exception (excluding SystemExit) or signal is encountered. Usage: - with ErrorHandler(cleanup_func): + handler = ErrorHandler(cleanup1_func, *cleanup1_args, **cleanup1_kwargs) + handler.register(cleanup2_func, *cleanup2_args, **cleanup2_kwargs) + + with handler: do_something() - If an exception is raised out of do_something, cleanup_func will be - called. The exception is not caught by the ErrorHandler. Similarly, - if a signal is encountered, cleanup_func is called followed by the - previously registered signal handler. + Or for one cleanup function: + + with ErrorHandler(func, args, kwargs): + do_something() + + If an exception is raised out of do_something, the cleanup functions will + be called in last in first out order. Then the exception is raised. + Similarly, if a signal is encountered, the cleanup functions are called + followed by the previously received signal handler. Each registered cleanup function is called exactly once. If a registered function raises an exception, it is logged and the next function is called. From 815887d4e4167102260f866cc4170abdfe31612b Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 5 Jul 2016 22:44:22 -0500 Subject: [PATCH 236/445] Refactor ErrorHandler.__exit__ also call_signal -> call_signals --- certbot/error_handler.py | 41 +++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index f3226de9d..20315ded7 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -61,22 +61,24 @@ class ErrorHandler(object): self._set_signal_handlers() def __exit__(self, exec_type, exec_value, trace): - self.body_executed = True - retval = False - if exec_type is errors.SignalExit: - logger.debug("Encountered signals: %s", self.received_signals) - self._call_registered() - for signum in self.received_signals: - self._call_signal(signum) - retval = True - # SystemExit is ignored to properly handle forks that don't exec - elif exec_type not in (None, SystemExit): - logger.debug("Encountered exception:\n%s", "".join( - traceback.format_exception(exec_type, exec_value, trace))) - self._call_registered() + try: + self.body_executed = True + retval = False + # SystemExit is ignored to properly handle forks that don't exec + if exec_type in (None, SystemExit): + return retval + elif exec_type is errors.SignalExit: + logger.debug("Encountered signals: %s", self.received_signals) + retval = True + else: + logger.debug("Encountered exception:\n%s", "".join( + traceback.format_exception(exec_type, exec_value, trace))) - self._reset_signal_handlers() - return retval + self._call_registered() + self._call_signals() + return retval + finally: + self._reset_signal_handlers() def register(self, func, *args, **kwargs): """Sets func to be called with *args and **kwargs during cleanup @@ -123,12 +125,13 @@ class ErrorHandler(object): if not self.body_executed: raise errors.SignalExit - def _call_signal(self, signum): + def _call_signals(self): """Calls the signal given by signum. :param int signum: signal number """ - logger.debug("Calling signal %s", signum) - signal.signal(signum, self.prev_handlers[signum]) - os.kill(os.getpid(), signum) + for signum in self.received_signals: + logger.debug("Calling signal %s", signum) + signal.signal(signum, self.prev_handlers[signum]) + os.kill(os.getpid(), signum) From 5e4f250110366e126e099ab41b1b2f4d6f10d50a Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 5 Jul 2016 22:46:23 -0500 Subject: [PATCH 237/445] Stylize _signal_handler docstring. --- certbot/error_handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index 20315ded7..779c2d5ca 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -115,7 +115,9 @@ class ErrorHandler(object): self.prev_handlers.clear() def _signal_handler(self, signum, unused_frame): - """Stores the recieved signal. If we are executing the code block in + """Replacement function for handling recieved signals. + + Store the recieved signal. If we are executing the code block in the body of the context manager, stop by raising signal exit. :param int signum: number of current signal From 8333829bbe53d5e20813285e483c8ac9776e984b Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 5 Jul 2016 22:47:42 -0500 Subject: [PATCH 238/445] Check that ErrorHandler raises exception. --- certbot/tests/error_handler_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index a7906c5ef..0974fe92a 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -46,7 +46,9 @@ class ErrorHandlerTest(unittest.TestCase): with self.handler: raise ValueError except ValueError: - pass + exception_raised = True + + self.assertTrue(exception_raised) self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) From 7ade835476d4afca12597fcf54f77bb4c7afca92 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 5 Jul 2016 22:57:13 -0500 Subject: [PATCH 239/445] import module instead of function --- certbot/tests/error_handler_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index 0974fe92a..a81d4a2d1 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -3,12 +3,12 @@ import os import signal import sys import unittest -from contextlib import contextmanager +import contextlib import mock -@contextmanager +@contextlib.contextmanager def signal_receiver(signums): """Context manager to catch signals""" signals = [] From e9f879a443e0f6d037ff9abeb11bd55fdf74a5df Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 5 Jul 2016 22:58:15 -0500 Subject: [PATCH 240/445] Use assertTrue and index in _SIGNALS --- certbot/tests/error_handler_test.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index a81d4a2d1..a242c4cab 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -56,13 +56,13 @@ class ErrorHandlerTest(unittest.TestCase): with signal_receiver(self.signals) as signals_received: with self.handler: should_be_42 = 42 - send_signal(signal.SIGTERM) + send_signal(self.signals[0]) should_be_42 *= 10 # check exectuion stoped when the signal was sent - assert 42 == should_be_42 + self.assertEqual(42, should_be_42) # assert signals were caught - assert [signal.SIGTERM] == signals_received + self.assertEqual([self.signals[0]], signals_received) # assert the error handling function was just called once self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) @@ -76,13 +76,14 @@ class ErrorHandlerTest(unittest.TestCase): bad_func.assert_called_once_with() def test_bad_recovery_with_signal(self): - bad_func = mock.MagicMock( - side_effect=lambda: send_signal(signal.SIGHUP)) + sig1 = self.signals[0] + sig2 = self.signals[-1] + bad_func = mock.MagicMock(side_effect=lambda: send_signal(sig1)) self.handler.register(bad_func) with signal_receiver(self.signals) as signals_received: with self.handler: - send_signal(signal.SIGTERM) - assert [signal.SIGTERM, signal.SIGHUP] == signals_received + send_signal(sig2) + self.assertEqual([sig2, sig1], signals_received) self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) bad_func.assert_called_once_with() From ece1f6a24d5abfe8991638ef4c5b5dd61ebbdc79 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Tue, 5 Jul 2016 23:28:57 -0500 Subject: [PATCH 241/445] Don't use _private functions in tests. --- certbot/tests/error_handler_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index a242c4cab..6cbc3c1fe 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -70,7 +70,11 @@ class ErrorHandlerTest(unittest.TestCase): def test_bad_recovery(self): bad_func = mock.MagicMock(side_effect=[ValueError]) self.handler.register(bad_func) - self.handler._call_registered() + try: + with self.handler: + raise ValueError + except ValueError: + pass self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) bad_func.assert_called_once_with() From 3d3908c8b31d66e495b0762627962f3f64b08b2e Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Wed, 6 Jul 2016 13:19:03 -0500 Subject: [PATCH 242/445] Update _call_signals docstring. --- certbot/error_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index 779c2d5ca..2140aec4f 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -128,7 +128,7 @@ class ErrorHandler(object): raise errors.SignalExit def _call_signals(self): - """Calls the signal given by signum. + """Finally call the deferred signals. :param int signum: signal number From 40983d67d7b4f1431619086ac0df19ddcf24f363 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Wed, 6 Jul 2016 13:19:58 -0500 Subject: [PATCH 243/445] Alphabetize imports in error_handler_test.py --- certbot/tests/error_handler_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index 6cbc3c1fe..58bc4850c 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -1,9 +1,9 @@ """Tests for certbot.error_handler.""" +import contextlib import os import signal import sys import unittest -import contextlib import mock From fd35a1c724ae2d1501fc64ac6be945fb5c7b8786 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Jul 2016 12:40:24 -0700 Subject: [PATCH 244/445] Explain why Apache [appears] not to be installed Would help debug #3244 --- certbot-apache/certbot_apache/configurator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 89d602f5f..d1c2b7165 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -157,8 +157,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): raise errors.NoInstallationError("Problem in Augeas installation") # Verify Apache is installed - if not util.exe_exists(constants.os_constant("restart_cmd")[0]): - raise errors.NoInstallationError + restart_cmd = constants.os_constant("restart_cmd")[0] + if not util.exe_exists(restart_cmd): + raise errors.NoInstallationError( + 'Cannot find Apache install ({0} not in PATH)'.format(restart_cmd)) # Make sure configuration is valid self.config_test() From 4b84538c8c9c0bb852eb2874cf38cc144dbd3f0d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Jul 2016 12:57:16 -0700 Subject: [PATCH 245/445] Address review comments --- certbot/display/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index 4e7d45741..39486b2bd 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -72,7 +72,7 @@ def _clean(dialog_result): elif code in (CANCEL, ESC): return (CANCEL, result) else: - logger.info("Surprising dialog return code %s", code) + logger.debug("Surprising dialog return code %s", code) return (CANCEL, result) @@ -83,6 +83,7 @@ class NcursesDisplay(object): def __init__(self, width=WIDTH, height=HEIGHT): super(NcursesDisplay, self).__init__() self.dialog = dialog.Dialog() + assert OK == self.dialog.DIALOG_OK, "What kind of absurdity is this?" self.width = width self.height = height @@ -189,7 +190,6 @@ class NcursesDisplay(object): :rtype: bool """ - assert OK == self.dialog.DIALOG_OK, "What kind of absurdity is this?" return self.dialog.DIALOG_OK == self.dialog.yesno( message, self.height, self.width, yes_label=yes_label, no_label=no_label) From 2e7f02805ce848b8cd4cea8cda944e5fb473c607 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Wed, 6 Jul 2016 15:59:51 -0500 Subject: [PATCH 246/445] Call _call_signals after _reset_signals. --- certbot/error_handler.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index 2140aec4f..e9deaea5c 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -75,10 +75,10 @@ class ErrorHandler(object): traceback.format_exception(exec_type, exec_value, trace))) self._call_registered() - self._call_signals() return retval finally: - self._reset_signal_handlers() + prev_handlers = self._reset_signal_handlers() + self._call_signals(prev_handlers) def register(self, func, *args, **kwargs): """Sets func to be called with *args and **kwargs during cleanup @@ -112,7 +112,9 @@ class ErrorHandler(object): """Resets signal handlers for signals in _SIGNALS.""" for signum in self.prev_handlers: signal.signal(signum, self.prev_handlers[signum]) + out = dict((k, v) for k, v in self.prev_handlers.items()) self.prev_handlers.clear() + return out def _signal_handler(self, signum, unused_frame): """Replacement function for handling recieved signals. @@ -127,7 +129,7 @@ class ErrorHandler(object): if not self.body_executed: raise errors.SignalExit - def _call_signals(self): + def _call_signals(self, prev_handlers): """Finally call the deferred signals. :param int signum: signal number @@ -135,5 +137,5 @@ class ErrorHandler(object): """ for signum in self.received_signals: logger.debug("Calling signal %s", signum) - signal.signal(signum, self.prev_handlers[signum]) + signal.signal(signum, prev_handlers[signum]) os.kill(os.getpid(), signum) From e28560514a8f9c6da979a8fc86d7b8c42544dd64 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Wed, 6 Jul 2016 16:56:11 -0500 Subject: [PATCH 247/445] Test that ErrorHandler resets default signals. --- certbot/tests/error_handler_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index 58bc4850c..a32f2a448 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -66,6 +66,9 @@ class ErrorHandlerTest(unittest.TestCase): # assert the error handling function was just called once self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) + for signum in self.signals: + sig = signal.getsignal(signum) + self.assertTrue((sig == signal.SIG_DFL) or (sig == signal.SIG_IGN)) def test_bad_recovery(self): bad_func = mock.MagicMock(side_effect=[ValueError]) From 83857baf30bd6a25ce29612885666a3fdd30abf6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Jul 2016 13:25:52 -0700 Subject: [PATCH 248/445] Update / cleanup installer error message Closes: #1756 Updating since we landed #788 and have shipped Apache support almost everywhere --- certbot/plugins/selection.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/certbot/plugins/selection.py b/certbot/plugins/selection.py index ac509d779..1d7fa323f 100644 --- a/certbot/plugins/selection.py +++ b/certbot/plugins/selection.py @@ -260,14 +260,9 @@ def diagnose_configurator_problem(cfg_type, requested, plugins): "your existing configuration.\nThe error was: {1!r}" .format(requested, plugins[requested].problem)) elif cfg_type == "installer": - if os.path.exists("/etc/debian_version"): - # Debian... installers are at least possible - msg = ('No installers seem to be present and working on your system; ' - 'fix that or try running certbot with the "certonly" command') - else: - # XXX update this logic as we make progress on #788 and nginx support - msg = ('No installers are available on your OS yet; try running ' - '"letsencrypt-auto certonly" to get a cert you can install manually') + msg = ('No installer plugins seem to be present and working on your system; ' + 'fix that or try running certbot with the "certonly" command to obtain' + ' a certificate you can install manually') else: msg = "{0} could not be determined or is not installed".format(cfg_type) raise errors.PluginSelectionError(msg) From 4a9846b91184fcd95947c12e95a6d1d8ceb6a928 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Jul 2016 20:02:40 -0700 Subject: [PATCH 249/445] Begin work on multi-topic help listings --- certbot/cli.py | 84 +++++++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 35b3b74ae..fe3a26c87 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -497,16 +497,23 @@ class HelpfulArgumentParser(object): pass return True - def add(self, topic, *args, **kwargs): + def add(self, topics, *args, **kwargs): """Add a new command line argument. - :param str: help topic this should be listed under, can be None for - "always documented" + :param topics: str or [str] help topic(s) this should be listed under, + or None for "always documented" :param list *args: the names of this argument flag :param dict **kwargs: various argparse settings for this argument """ + if isinstance(topics, list): + # if this flag can be listed in multiple sections, try to pick the one + # that the user has asked for help about + topic = self.help_arg if self.help_arg in topics else topics[0] + else: + topic = topics # there's only one + if self.detect_defaults: kwargs = self.modify_kwargs_for_default_detection(**kwargs) @@ -607,6 +614,34 @@ class HelpfulArgumentParser(object): else: return dict([(t, t == chosen_topic) for t in self.help_topics]) +def _add_all_groups(helpful): + helpful.add_group("automation", description="Arguments for automating execution & other tweaks") + helpful.add_group("security", description="Security parameters & server settings") + helpful.add_group( + "testing", description="The following flags are meant for " + "testing purposes only! Do NOT change them, unless you " + "really know what you're doing!") + # VERBS + helpful.add_group( + "renew", description="The 'renew' subcommand will attempt to renew all" + " certificates (or more precisely, certificate lineages) you have" + " previously obtained if they are close to expiry, and print a" + " summary of the results. By default, 'renew' will reuse the options" + " used to create obtain or most recently successfully renew each" + " certificate lineage. You can try it with `--dry-run` first. For" + " more fine-grained control, you can renew individual lineages with" + " the `certonly` subcommand. Hooks are available to run commands " + " before and after renewal; see" + " https://certbot.eff.org/docs/using.html#renewal for more information on these.") + helpful.add_group("certonly", description="Options for modifying how a cert is obtained") + helpful.add_group("install", description="Options for modifying how a cert is deployed") + helpful.add_group("revoke", description="Options for revocation of certs") + helpful.add_group("rollback", description="Options for reverting config changes") + helpful.add_group("plugins", description='Options for the "plugins" subcommand') + helpful.add_group("config_changes", + description="Options for showing a history of config changes") + helpful.add_group("paths", description="Arguments changing execution paths & servers") + def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: disable=too-many-statements """Returns parsed command line arguments. @@ -622,6 +657,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis # pylint: disable=too-many-statements helpful = HelpfulArgumentParser(args, plugins, detect_defaults) + _add_all_groups(helpful) # --help is automatically provided by argparse helpful.add( @@ -678,11 +714,8 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="Domain names to apply. For multiple domains you can use " "multiple -d flags or enter a comma separated list of domains " "as a parameter.") - helpful.add_group( - "automation", - description="Arguments for automating execution & other tweaks") helpful.add( - "automation", "--keep-until-expiring", "--keep", "--reinstall", + ["automation", "renew"], "--keep-until-expiring", "--keep", "--reinstall", dest="reinstall", action="store_true", help="If the requested cert matches an existing cert, always keep the " "existing one until it is due for renewal (for the " @@ -721,23 +754,19 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "(both can be renewed in parallel)") helpful.add( "automation", "--os-packages-only", action="store_true", - help="(letsencrypt-auto only) install OS package dependencies and then stop") + help="(certbot-auto only) install OS package dependencies and then stop") helpful.add( "automation", "--no-self-upgrade", action="store_true", - help="(letsencrypt-auto only) prevent the letsencrypt-auto script from" + help="(certbot-auto only) prevent the certbot-auto script from" " upgrading itself to newer released versions") helpful.add( "automation", "-q", "--quiet", dest="quiet", action="store_true", help="Silence all output except errors. Useful for automation via cron." " Implies --non-interactive.") - helpful.add_group( - "testing", description="The following flags are meant for " - "testing purposes only! Do NOT change them, unless you " - "really know what you're doing!") helpful.add( "testing", "--debug", action="store_true", - help="Show tracebacks in case of errors, and allow letsencrypt-auto " + help="Show tracebacks in case of errors, and allow certbot-auto " "execution on experimental platforms") helpful.add( "testing", "--no-verify-ssl", action="store_true", @@ -754,8 +783,6 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "testing", "--break-my-certs", action="store_true", help="Be willing to replace or renew valid certs with invalid " "(testing/staging) certs") - helpful.add_group( - "security", description="Security parameters & server settings") helpful.add( "security", "--rsa-key-size", type=int, metavar="N", default=flag_default("rsa_key_size"), help=config_help("rsa_key_size")) @@ -805,18 +832,6 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") - helpful.add_group( - "renew", description="The 'renew' subcommand will attempt to renew all" - " certificates (or more precisely, certificate lineages) you have" - " previously obtained if they are close to expiry, and print a" - " summary of the results. By default, 'renew' will reuse the options" - " used to create obtain or most recently successfully renew each" - " certificate lineage. You can try it with `--dry-run` first. For" - " more fine-grained control, you can renew individual lineages with" - " the `certonly` subcommand. Hooks are available to run commands " - " before and after renewal; see" - " https://certbot.eff.org/docs/using.html#renewal for more information on these.") - helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates. Intended" @@ -859,13 +874,6 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis def _create_subparsers(helpful): - helpful.add_group("certonly", description="Options for modifying how a cert is obtained") - helpful.add_group("install", description="Options for modifying how a cert is deployed") - helpful.add_group("revoke", description="Options for revocation of certs") - helpful.add_group("rollback", description="Options for reverting config changes") - helpful.add_group("plugins", description="Plugin options") - helpful.add_group("config_changes", - description="Options for showing a history of config changes") helpful.add("config_changes", "--num", type=int, help="How many past revisions you want to be displayed") helpful.add( @@ -901,8 +909,6 @@ def _paths_parser(helpful): verb = helpful.verb if verb == "help": verb = helpful.help_arg - helpful.add_group( - "paths", description="Arguments changing execution paths & servers") cph = "Path to where cert is saved (with auth --csr), installed from or revoked." section = "paths" @@ -948,12 +954,14 @@ def _paths_parser(helpful): def _plugins_parsing(helpful, plugins): + # It's nuts, but there are two "plugins" topics. Somehow this works helpful.add_group( - "plugins", description="Certbot client supports an " + "plugins", description="Plugin Selection: Certbot client supports an " "extensible plugins architecture. See '%(prog)s plugins' for a " "list of all installed plugins and their names. You can force " "a particular plugin by setting options provided below. Running " "--help will list flags specific to that plugin.") + helpful.add( "plugins", "-a", "--authenticator", help="Authenticator plugin name.") helpful.add( From d96278505e6b01f115c597aaf035452924714425 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Jul 2016 20:07:33 -0700 Subject: [PATCH 250/445] Fix test --- certbot/tests/cli_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 896550837..388c38152 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -113,7 +113,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods out = self._help_output(['--help', 'plugins']) self.assertTrue("--manual-test-mode" not in out) self.assertTrue("--prepare" in out) - self.assertTrue("Plugin options" in out) + self.assertTrue('"plugins" subcommand' in out) out = self._help_output(['--help', 'install']) self.assertTrue("--cert-path" in out) From fd35e407ca221e145805cdb88d76dc8b094d4e2d Mon Sep 17 00:00:00 2001 From: Robert Buchholz Date: Thu, 7 Jul 2016 11:20:52 +0200 Subject: [PATCH 251/445] Reference certbot-auto in CLI help --- certbot/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 35b3b74ae..470267029 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -721,10 +721,10 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "(both can be renewed in parallel)") helpful.add( "automation", "--os-packages-only", action="store_true", - help="(letsencrypt-auto only) install OS package dependencies and then stop") + help="(certbot-auto only) install OS package dependencies and then stop") helpful.add( "automation", "--no-self-upgrade", action="store_true", - help="(letsencrypt-auto only) prevent the letsencrypt-auto script from" + help="(certbot-auto only) prevent the certbot-auto script from" " upgrading itself to newer released versions") helpful.add( "automation", "-q", "--quiet", dest="quiet", action="store_true", @@ -737,7 +737,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "really know what you're doing!") helpful.add( "testing", "--debug", action="store_true", - help="Show tracebacks in case of errors, and allow letsencrypt-auto " + help="Show tracebacks in case of errors, and allow certbot-auto " "execution on experimental platforms") helpful.add( "testing", "--no-verify-ssl", action="store_true", From 204c3f0dfbd242751efaf7a36ced35f7e5d16ca5 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 7 Jul 2016 15:11:23 -0700 Subject: [PATCH 252/445] Start using multi-topic CLI flag documentation --- certbot/cli.py | 53 +++++++++++++++++++++------------------ certbot/tests/cli_test.py | 8 ++++++ 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index fe3a26c87..5b7f6ca8a 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -88,8 +88,8 @@ More detailed help: the available topics are: all, automation, paths, security, testing, or any of the subcommands or - plugins (certonly, install, register, nginx, apache, standalone, webroot, - etc.) + plugins (certonly, renew, install, register, nginx, apache, standalone, + webroot, etc.) """ @@ -390,8 +390,7 @@ class HelpfulArgumentParser(object): if getattr(parsed_args, arg): raise errors.Error( ("Conflicting values for displayer." - " {0} conflicts with dialog_mode").format(arg) - ) + " {0} conflicts with dialog_mode").format(arg)) if parsed_args.validate_hooks: hooks.validate_hooks(parsed_args) @@ -676,9 +675,18 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "which ones are required if it finds one missing") helpful.add( None, "--dialog", dest="dialog_mode", action="store_true", - help="Run using dialog") + help="Run using interactive dialog menus") helpful.add( - None, "--dry-run", action="store_true", dest="dry_run", + [None, "run", "certonly"], + "-d", "--domains", "--domain", dest="domains", + metavar="DOMAIN", action=_DomainsAction, default=[], + help="Domain names to apply. For multiple domains you can use " + "multiple -d flags or enter a comma separated list of domains " + "as a parameter.") + + helpful.add( + [None, "testing", "renew", "certonly"], + "--dry-run", action="store_true", dest="dry_run", help="Perform a test run of the client, obtaining test (invalid) certs" " but not saving them to disk. This can currently only be used" " with the 'certonly' and 'renew' subcommands. \nNote: Although --dry-run" @@ -705,17 +713,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "with an existing registration, such as the e-mail address, " "should be updated, rather than registering a new account.") helpful.add(None, "-m", "--email", help=config_help("email")) - # positional arg shadows --domains, instead of appending, and - # --domains is useful, because it can be stored in config - #for subparser in parser_run, parser_auth, parser_install: - # subparser.add_argument("domains", nargs="*", metavar="domain") - helpful.add(None, "-d", "--domains", "--domain", dest="domains", - metavar="DOMAIN", action=_DomainsAction, default=[], - help="Domain names to apply. For multiple domains you can use " - "multiple -d flags or enter a comma separated list of domains " - "as a parameter.") helpful.add( - ["automation", "renew"], "--keep-until-expiring", "--keep", "--reinstall", + ["automation", "renew", "certonly", "run"], + "--keep-until-expiring", "--keep", "--reinstall", dest="reinstall", action="store_true", help="If the requested cert matches an existing cert, always keep the " "existing one until it is due for renewal (for the " @@ -729,14 +729,16 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis version="%(prog)s {0}".format(certbot.__version__), help="show program's version number and exit") helpful.add( - "automation", "--force-renewal", "--renew-by-default", + ["automation", "renew"], + "--force-renewal", "--renew-by-default", action="store_true", dest="renew_by_default", help="If a certificate " "already exists for the requested domains, renew it now, " "regardless of whether it is near expiry. (Often " "--keep-until-expiring is more appropriate). Also implies " "--expand.") helpful.add( - "automation", "--allow-subset-of-names", action="store_true", + ["automation", "renew"], + "--allow-subset-of-names", action="store_true", help="When performing domain validation, do not consider it a failure " "if authorizations can not be obtained for a strict subset of " "the requested domains. This may be useful for allowing renewals for " @@ -760,7 +762,8 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis help="(certbot-auto only) prevent the certbot-auto script from" " upgrading itself to newer released versions") helpful.add( - "automation", "-q", "--quiet", dest="quiet", action="store_true", + ["automation", "renew"], + "-q", "--quiet", dest="quiet", action="store_true", help="Silence all output except errors. Useful for automation via cron." " Implies --non-interactive.") @@ -970,15 +973,17 @@ def _plugins_parsing(helpful, plugins): "plugins", "--configurator", help="Name of the plugin that is " "both an authenticator and an installer. Should not be used " "together with --authenticator or --installer.") - helpful.add("plugins", "--apache", action="store_true", + helpful.add(["plugins", "certonly", "run", "install"], + "--apache", action="store_true", help="Obtain and install certs using Apache") - helpful.add("plugins", "--nginx", action="store_true", + helpful.add(["plugins", "certonly", "run", "install"], + "--nginx", action="store_true", help="Obtain and install certs using Nginx") - helpful.add("plugins", "--standalone", action="store_true", + helpful.add(["plugins", "certonly"], "--standalone", action="store_true", help='Obtain certs using a "standalone" webserver.') - helpful.add("plugins", "--manual", action="store_true", + helpful.add(["plugins", "certonly"], "--manual", action="store_true", help='Provide laborious manual instructions for obtaining a cert') - helpful.add("plugins", "--webroot", action="store_true", + helpful.add(["plugins", "certonly"], "--webroot", action="store_true", help='Obtain certs by placing files in a webroot directory.') # things should not be reorder past/pre this comment: diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 388c38152..63c682b20 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -115,6 +115,14 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue("--prepare" in out) self.assertTrue('"plugins" subcommand' in out) + # test multiple topics + out = self._help_output(['-h', 'renew']) + self.assertTrue("--keep" in out) + out = self._help_output(['-h', 'automation']) + self.assertTrue("--keep" in out) + out = self._help_output(['-h', 'revoke']) + self.assertTrue("--keep" not in out) + out = self._help_output(['--help', 'install']) self.assertTrue("--cert-path" in out) self.assertTrue("--key-path" in out) From c92a1cd182f4ad15e40552777648621b08dab486 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 7 Jul 2016 17:24:58 -0700 Subject: [PATCH 253/445] Clarifications to Docker, certbot-auto content reflecting first three questions in my comment https://github.com/certbot/certbot/pull/3232#issuecomment-231154320 --- docs/using.rst | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 806dfb340..1d9dc0c32 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -8,14 +8,16 @@ User Guide Getting Certbot =============== -To get specific instructions for installing Certbot on your OS, we recommend -visiting certbot.eff.org_. If you're offline, you can find some general +To get specific instructions for installing Certbot on your OS, +visit certbot.eff.org_. This is the easiest way to install Certbot. + +If you're offline, or if your webserver or OS are not in the menu, you can find some general instructions `in the README / Introduction `__ __ installation_ .. _certbot.eff.org: https://certbot.eff.org -.. _certbot-auto: +.. _certbot-auto: https://certbot.eff.org/docs/using.html#certbot-auto The name of the certbot command ------------------------------- @@ -394,7 +396,12 @@ Running with Docker Docker_ is an amazingly simple and quick way to obtain a certificate. However, this mode of operation is unable to install certificates or configure your webserver, because our installer -plugins cannot reach it from inside the Docker container. +plugins cannot reach your webserver from inside the Docker container. + +Most users should use the operating system packages (available from +certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only +use Docker if you are sure you know what you are doing and have a +good reason to do so. You should definitely read the :ref:`where-certs` section, in order to know how to manage the certs @@ -415,9 +422,13 @@ to, `install Docker`_, then issue the following command: -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ quay.io/letsencrypt/letsencrypt:latest auth -and follow the instructions (note that ``auth`` command is explicitly -used - no installer plugins involved). Your new cert will be available -in ``/etc/letsencrypt/live`` on the host. +Certbot will obtain a certificate and place it in the directory +``/etc/letsencrypt/live`` on your system and display further instructions +for installing the certificates. You must use the ``auth`` command +to install the certificates instead of plug-ins for this method. + +For more information about the layout +of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. .. _Docker: https://docker.com .. _`install Docker`: https://docs.docker.com/userguide/ @@ -543,10 +554,10 @@ whole process is described in the :doc:`contributing`. Comparison of different methods ------------------------------- -Unless you have a very specific requirements, we kindly suggest that you use -the certbot-auto_ method. It's the fastest, the most thoroughly -tested and the most reliable way of getting our software and the free -TLS/SSL certificates! +Unless you have very specific requirements, we kindly suggest that you use +the Certbot packages provided by your package manager (see certbot.eff.org_). +If such packages are not available, we recommend using ``certbot-auto``, which +automates the process of installing Certbot on your system. Beyond the methods discussed here, other methods may be possible, such as installing Certbot directly with pip from PyPI or downloading a ZIP From 44113a5d068ef224295994fcd60f9024b0a6854d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 7 Jul 2016 17:25:09 -0700 Subject: [PATCH 254/445] Automatically enable EPEL (after prompting users) --- letsencrypt-auto-source/letsencrypt-auto | 24 +++++++++++++++---- .../pieces/bootstrappers/rpm_common.sh | 24 +++++++++++++++---- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 22c914ed8..4abe7be38 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -281,6 +281,26 @@ BootstrapRpmCommon() { exit 1 fi + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool list *virtualenv > /dev/null 2>&1; then + echo "To use Certbot, packages from the EPEL repository need to be installed." + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Enabling the EPEL repository in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + sleep 1s + fi + if ! $SUDO $tool install $yes_flag epel-release; then + echo "Could not enable EPEL. Aborting bootstrap!" + exit 1 + fi + fi + pkgs=" gcc dialog @@ -318,10 +338,6 @@ BootstrapRpmCommon() { " fi - if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" - fi - if ! $SUDO $tool install $yes_flag $pkgs; then echo "Could not install OS dependencies. Aborting bootstrap!" exit 1 diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh index 0f98b4bbc..e9865aed3 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh @@ -17,6 +17,26 @@ BootstrapRpmCommon() { exit 1 fi + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool list *virtualenv > /dev/null 2>&1; then + echo "To use Certbot, packages from the EPEL repository need to be installed." + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Enabling the EPEL repository in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." + sleep 1s + fi + if ! $SUDO $tool install $yes_flag epel-release; then + echo "Could not enable EPEL. Aborting bootstrap!" + exit 1 + fi + fi + pkgs=" gcc dialog @@ -54,10 +74,6 @@ BootstrapRpmCommon() { " fi - if [ "$ASSUME_YES" = 1 ]; then - yes_flag="-y" - fi - if ! $SUDO $tool install $yes_flag $pkgs; then echo "Could not install OS dependencies. Aborting bootstrap!" exit 1 From 4b4a02a7af221116c2820af310cab317f78c4ed3 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Thu, 7 Jul 2016 20:07:13 -0500 Subject: [PATCH 255/445] Remove extra tracking of prev_handlers. --- certbot/error_handler.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index e9deaea5c..68bd7f754 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -77,8 +77,8 @@ class ErrorHandler(object): self._call_registered() return retval finally: - prev_handlers = self._reset_signal_handlers() - self._call_signals(prev_handlers) + self._reset_signal_handlers() + self._call_signals() def register(self, func, *args, **kwargs): """Sets func to be called with *args and **kwargs during cleanup @@ -112,9 +112,7 @@ class ErrorHandler(object): """Resets signal handlers for signals in _SIGNALS.""" for signum in self.prev_handlers: signal.signal(signum, self.prev_handlers[signum]) - out = dict((k, v) for k, v in self.prev_handlers.items()) self.prev_handlers.clear() - return out def _signal_handler(self, signum, unused_frame): """Replacement function for handling recieved signals. @@ -129,13 +127,8 @@ class ErrorHandler(object): if not self.body_executed: raise errors.SignalExit - def _call_signals(self, prev_handlers): - """Finally call the deferred signals. - - :param int signum: signal number - - """ + def _call_signals(self): + """Finally call the deferred signals.""" for signum in self.received_signals: logger.debug("Calling signal %s", signum) - signal.signal(signum, prev_handlers[signum]) os.kill(os.getpid(), signum) From ecd1ca4645fe594df805d1e4b6afa2c6b4441245 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 8 Jul 2016 10:05:56 -0700 Subject: [PATCH 256/445] grammatical improvement --- certbot/cli.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/certbot/cli.py b/certbot/cli.py index 5b7f6ca8a..89dacac0d 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -828,13 +828,10 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "security", "--no-staple-ocsp", action="store_false", help="Do not automatically enable OCSP Stapling.", dest="staple", default=None) - - helpful.add( "security", "--strict-permissions", action="store_true", help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") - helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates. Intended" From c3244df951fefdbc5158f59383f6695978dee1a6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 8 Jul 2016 10:12:57 -0700 Subject: [PATCH 257/445] more doc improvements --- certbot-apache/certbot_apache/configurator.py | 2 +- certbot/cli.py | 9 ++++----- certbot/interfaces.py | 2 +- certbot/plugins/standalone.py | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index d1c2b7165..50fd10895 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -83,7 +83,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ - description = "Apache Web Server - Alpha" + description = "Apache Web Server plugin - Beta" @classmethod def add_parser_arguments(cls, add): diff --git a/certbot/cli.py b/certbot/cli.py index 89dacac0d..97dfc6d7e 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -766,7 +766,10 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis "-q", "--quiet", dest="quiet", action="store_true", help="Silence all output except errors. Useful for automation via cron." " Implies --non-interactive.") - + # overwrites server, handled in HelpfulArgumentParser.parse_args() + helpful.add("testing", "--test-cert", "--staging", action='store_true', dest='staging', + help='Use the staging server to obtain test (invalid) certs; equivalent' + ' to --server ' + constants.STAGING_URI) helpful.add( "testing", "--debug", action="store_true", help="Show tracebacks in case of errors, and allow certbot-auto " @@ -947,10 +950,6 @@ def _paths_parser(helpful): help="Logs directory.") add("paths", "--server", default=flag_default("server"), help=config_help("server")) - # overwrites server, handled in HelpfulArgumentParser.parse_args() - add("testing", "--test-cert", "--staging", action='store_true', dest='staging', - help='Use the staging server to obtain test (invalid) certs; equivalent' - ' to --server ' + constants.STAGING_URI) def _plugins_parsing(helpful, plugins): diff --git a/certbot/interfaces.py b/certbot/interfaces.py index e4e62e0a2..d4b391378 100644 --- a/certbot/interfaces.py +++ b/certbot/interfaces.py @@ -227,7 +227,7 @@ class IConfig(zope.interface.Interface): "Location of renewal configuration file.") no_verify_ssl = zope.interface.Attribute( - "Disable SSL certificate verification.") + "Disable verification of the ACME server's certificate.") tls_sni_01_port = zope.interface.Attribute( "Port number to perform tls-sni-01 challenge. " "Boulder in testing mode defaults to 5001.") diff --git a/certbot/plugins/standalone.py b/certbot/plugins/standalone.py index 8e1cb72a4..97aca351a 100644 --- a/certbot/plugins/standalone.py +++ b/certbot/plugins/standalone.py @@ -154,7 +154,7 @@ class Authenticator(common.Plugin): rely on any existing server program. """ - description = "Automatically use a temporary webserver" + description = "Spin up a temporary webserver" def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) From 40449ed2747634ae77928cae45424e5031a66c5b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 8 Jul 2016 12:49:41 -0700 Subject: [PATCH 258/445] Add single _PERM_ERR_FMT string --- certbot/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot/main.py b/certbot/main.py index be68d694e..8e47e736a 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -36,6 +36,12 @@ from certbot.display import util as display_util, ops as display_ops from certbot.plugins import disco as plugins_disco from certbot.plugins import selection as plug_sel + +_PERM_ERR_FMT = ("An error occurred while trying to create or modify {0}. To " + "run as non-root, set --config-dir, --logs-dir, and " + "--work-dir to writeable paths.") + + logger = logging.getLogger(__name__) From 9ae755ef4cf3f8f4978d7c5594d6d16ed835a1f5 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 8 Jul 2016 12:53:09 -0700 Subject: [PATCH 259/445] simplify log file error handling --- certbot/main.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 8e47e736a..d3f90926a 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -2,7 +2,6 @@ from __future__ import print_function import atexit import dialog -import errno import functools import logging.handlers import os @@ -602,13 +601,8 @@ def setup_log_file_handler(config, logfile, fmt): try: handler = logging.handlers.RotatingFileHandler( log_file_path, maxBytes=2 ** 20, backupCount=10) - except IOError as e: - if e.errno == errno.EACCES: - msg = ("Access denied writing to {0}. To run as non-root, set " + - "--logs-dir, --config-dir, --work-dir to writable paths.") - raise errors.Error(msg.format(log_file_path)) - else: - raise + except IOError: + raise errors.Error(_PERM_ERR_FMT.format(log_file_path)) # rotate on each invocation, rollover only possible when maxBytes # is nonzero and backupCount is nonzero, so we set maxBytes as big # as possible not to overrun in single CLI invocation (1MB). From f3c6bac31065a200e1a2b79579c907279d48af1b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 8 Jul 2016 13:02:28 -0700 Subject: [PATCH 260/445] stop spacing out --- certbot/tests/main_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 66cba64a3..d044a50b7 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1,10 +1,8 @@ """Tests for certbot.main.""" import unittest - import mock - from certbot import cli from certbot import configuration from certbot.plugins import disco as plugins_disco From 4f35f3fdf7e8631282d11d3c7a854770dd02dccf Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 8 Jul 2016 13:07:49 -0700 Subject: [PATCH 261/445] Add SetupLogFileHandlerTest --- certbot/tests/main_test.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index d044a50b7..5f6723bd7 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1,10 +1,13 @@ """Tests for certbot.main.""" +import shutil +import tempfile import unittest import mock from certbot import cli from certbot import configuration +from certbot import errors from certbot.plugins import disco as plugins_disco @@ -42,5 +45,26 @@ class ObtainCertTest(unittest.TestCase): self.assertFalse(pause) +class SetupLogFileHandlerTest(unittest.TestCase): + """Tests for certbot.main.setup_log_file_handler.""" + + def setUp(self): + self.config = mock.Mock(spec_set=['logs_dir'], + logs_dir=tempfile.mkdtemp()) + + def tearDown(self): + shutil.rmtree(self.config.logs_dir) + + def _call(self, *args, **kwargs): + from certbot.main import setup_log_file_handler + return setup_log_file_handler(*args, **kwargs) + + @mock.patch('certbot.main.logging.handlers.RotatingFileHandler') + def test_ioerror(self, mock_handler): + mock_handler.side_effect = IOError + self.assertRaises(errors.Error, self._call, + self.config, "test.log", "%s") + + if __name__ == '__main__': unittest.main() # pragma: no cover From 754b7956b3003cc75b2b1a11e40d86a6bf6828f3 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 6 Jul 2016 15:49:22 -0700 Subject: [PATCH 262/445] Make the error even more informative --- certbot-apache/certbot_apache/configurator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index d1c2b7165..0c95fe18e 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -159,8 +159,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Verify Apache is installed restart_cmd = constants.os_constant("restart_cmd")[0] if not util.exe_exists(restart_cmd): + logger.warn("Failed to find %s in PATH: %s", restart_cmd, os.environ["PATH"]) raise errors.NoInstallationError( - 'Cannot find Apache install ({0} not in PATH)'.format(restart_cmd)) + 'Cannot find Apache control command {0}'.format(restart_cmd)) # Make sure configuration is valid self.config_test() From a322f44f2b7c0ef0302de956ed068671cf4ef32f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 7 Jul 2016 17:54:39 -0700 Subject: [PATCH 263/445] Implement PATH fallback for apachectl search --- certbot-apache/certbot_apache/configurator.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 0c95fe18e..c9a00a64e 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -159,9 +159,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Verify Apache is installed restart_cmd = constants.os_constant("restart_cmd")[0] if not util.exe_exists(restart_cmd): - logger.warn("Failed to find %s in PATH: %s", restart_cmd, os.environ["PATH"]) - raise errors.NoInstallationError( - 'Cannot find Apache control command {0}'.format(restart_cmd)) + # mitigate https://github.com/certbot/certbot/issues/1833 + logger.debug("Can't find %s, attempting PATH mitigation by adding " + "/usr/sbin/ and /usr/local/bin/", restart_cmd) + os.environ["PATH"] = os.pathsep.join((os.environ["PATH"], "/usr/sbin/", + "/usr/local/bin/")) + if not util.exe_exists(restart_cmd): + logger.warn("Failed to find %s in expanded PATH: %s", + restart_cmd, os.environ["PATH"]) + raise errors.NoInstallationError( + 'Cannot find Apache control command {0}'.format(restart_cmd)) # Make sure configuration is valid self.config_test() From cecac803a09c8c934f6fbe14bbe9204467d7174b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 7 Jul 2016 18:17:45 -0700 Subject: [PATCH 264/445] Do this more cleanly --- certbot-apache/certbot_apache/configurator.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index c9a00a64e..329e62135 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -141,6 +141,20 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST) + def _path_surgery(self): + """Mitigate https://github.com/certbot/certbot/issues/1833""" + dirs = ("/usr/sbin/", "/usr/local/bin/", "/usr/local/sbin/") + path = os.environ["PATH"] + added = [] + for d in dirs: + if d not in path: + path += os.pathsep + d + added.append(d) + if any(added): + logger.debug("Can't find %s, attempting PATH mitigation by adding %s" + restart_cmd, os.pathsep.join(added)) + os.environ["PATH"] = path + def prepare(self): """Prepare the authenticator/installer. @@ -159,11 +173,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Verify Apache is installed restart_cmd = constants.os_constant("restart_cmd")[0] if not util.exe_exists(restart_cmd): - # mitigate https://github.com/certbot/certbot/issues/1833 - logger.debug("Can't find %s, attempting PATH mitigation by adding " - "/usr/sbin/ and /usr/local/bin/", restart_cmd) - os.environ["PATH"] = os.pathsep.join((os.environ["PATH"], "/usr/sbin/", - "/usr/local/bin/")) + self._path_surgery() if not util.exe_exists(restart_cmd): logger.warn("Failed to find %s in expanded PATH: %s", restart_cmd, os.environ["PATH"]) From 757a8ddae7c5ac1a8acd500cda9b1f7505fc4963 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 8 Jul 2016 00:37:52 -0700 Subject: [PATCH 265/445] Fixes & tests --- certbot-apache/certbot_apache/configurator.py | 20 +++++++++----- .../certbot_apache/tests/configurator_test.py | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 329e62135..12cba34f1 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -141,9 +141,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST) - def _path_surgery(self): - """Mitigate https://github.com/certbot/certbot/issues/1833""" - dirs = ("/usr/sbin/", "/usr/local/bin/", "/usr/local/sbin/") + def _path_surgery(self, restart_cmd): + """Mitigate https://github.com/certbot/certbot/issues/1833 + + :returns: " expanded" if an expansion of the PATH occurred; + "" otherwise + """ + dirs = ("/usr/sbin", "/usr/local/bin", "/usr/local/sbin") path = os.environ["PATH"] added = [] for d in dirs: @@ -151,9 +155,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): path += os.pathsep + d added.append(d) if any(added): - logger.debug("Can't find %s, attempting PATH mitigation by adding %s" + logger.debug("Can't find %s, attempting PATH mitigation by adding %s", restart_cmd, os.pathsep.join(added)) os.environ["PATH"] = path + return " expanded" + return "" def prepare(self): """Prepare the authenticator/installer. @@ -173,10 +179,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Verify Apache is installed restart_cmd = constants.os_constant("restart_cmd")[0] if not util.exe_exists(restart_cmd): - self._path_surgery() + expanded = self._path_surgery(restart_cmd) if not util.exe_exists(restart_cmd): - logger.warn("Failed to find %s in expanded PATH: %s", - restart_cmd, os.environ["PATH"]) + logger.warn("Failed to find %s in %s PATH: %s", + restart_cmd, expanded, os.environ["PATH"]) raise errors.NoInstallationError( 'Cannot find Apache control command {0}'.format(restart_cmd)) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 9a034c3e0..d5139912e 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -86,6 +86,32 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises( errors.NotSupportedError, self.config.prepare) + @mock.patch("certbot_apache.configurator.logger.debug") + def test_path_surgery(self, mock_debug): + # pylint: disable=protected-access + all_path = {"PATH": "/usr/local/bin:/bin/:/usr/sbin/:/usr/local/sbin/"} + with mock.patch.dict('os.environ', all_path): + self.config._path_surgery("thingy") + self.assertEquals(mock_debug.call_count, 0) + self.assertEquals(os.environ["PATH"], all_path["PATH"]) + no_path = {"PATH": "/tmp/"} + with mock.patch.dict('os.environ', no_path): + self.config._path_surgery("thingy") + self.assertEquals(mock_debug.call_count, 1) + self.assertTrue("/usr/local/bin" in os.environ["PATH"]) + self.assertTrue("/tmp" in os.environ["PATH"]) + + @mock.patch("certbot_apache.configurator.ApacheConfigurator.init_augeas") + @mock.patch("certbot_apache.configurator.ApacheConfigurator._path_surgery") + @mock.patch("certbot_apache.configurator.logger.warn") + def test_no_install(self, mock_warn, mock_surgery, _init_augeas): + silly_path = {"PATH": "/tmp/nothingness2342"} + with mock.patch.dict('os.environ', silly_path): + self.assertRaises(errors.NoInstallationError, self.config.prepare) + self.assertEquals(mock_warn.call_count, 1) + self.assertEquals(mock_surgery.call_count, 1) + self.assertTrue("Failed to find" in mock_warn.call_args[0][0]) + def test_add_parser_arguments(self): # pylint: disable=no-self-use from certbot_apache.configurator import ApacheConfigurator # Weak test.. From 0bedeb449a239e79ccba3989c12ba07b3c93e363 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 8 Jul 2016 13:58:39 -0700 Subject: [PATCH 266/445] Refactor path_surgery into plugins.util so that nginx can call it --- certbot-apache/certbot_apache/configurator.py | 25 +----------- .../certbot_apache/tests/configurator_test.py | 38 ++++--------------- certbot/plugins/util.py | 30 +++++++++++++++ certbot/plugins/util_test.py | 24 ++++++++++++ 4 files changed, 64 insertions(+), 53 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 12cba34f1..74aab242e 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -18,6 +18,7 @@ from certbot import interfaces from certbot import util from certbot.plugins import common +from certbot.plugins.util import path_surgery from certbot_apache import augeas_configurator from certbot_apache import constants @@ -141,25 +142,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST) - def _path_surgery(self, restart_cmd): - """Mitigate https://github.com/certbot/certbot/issues/1833 - - :returns: " expanded" if an expansion of the PATH occurred; - "" otherwise - """ - dirs = ("/usr/sbin", "/usr/local/bin", "/usr/local/sbin") - path = os.environ["PATH"] - added = [] - for d in dirs: - if d not in path: - path += os.pathsep + d - added.append(d) - if any(added): - logger.debug("Can't find %s, attempting PATH mitigation by adding %s", - restart_cmd, os.pathsep.join(added)) - os.environ["PATH"] = path - return " expanded" - return "" def prepare(self): """Prepare the authenticator/installer. @@ -179,10 +161,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Verify Apache is installed restart_cmd = constants.os_constant("restart_cmd")[0] if not util.exe_exists(restart_cmd): - expanded = self._path_surgery(restart_cmd) - if not util.exe_exists(restart_cmd): - logger.warn("Failed to find %s in %s PATH: %s", - restart_cmd, expanded, os.environ["PATH"]) + if not path_surgery(restart_cmd): raise errors.NoInstallationError( 'Cannot find Apache control command {0}'.format(restart_cmd)) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index d5139912e..eac16c7fe 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -49,11 +49,14 @@ class MultipleVhostsTest(util.ApacheTest): shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) - @mock.patch("certbot_apache.configurator.util.exe_exists") - def test_prepare_no_install(self, mock_exe_exists): - mock_exe_exists.return_value = False - self.assertRaises( - errors.NoInstallationError, self.config.prepare) + @mock.patch("certbot_apache.configurator.ApacheConfigurator.init_augeas") + @mock.patch("certbot_apache.configurator.path_surgery") + def test_prepare_no_install(self, mock_surgery, _init_augeas): + silly_path = {"PATH": "/tmp/nothingness2342"} + mock_surgery.return_value = False + with mock.patch.dict('os.environ', silly_path): + self.assertRaises(errors.NoInstallationError, self.config.prepare) + self.assertEquals(mock_surgery.call_count, 1) @mock.patch("certbot_apache.augeas_configurator.AugeasConfigurator.init_augeas") def test_prepare_no_augeas(self, mock_init_augeas): @@ -86,31 +89,6 @@ class MultipleVhostsTest(util.ApacheTest): self.assertRaises( errors.NotSupportedError, self.config.prepare) - @mock.patch("certbot_apache.configurator.logger.debug") - def test_path_surgery(self, mock_debug): - # pylint: disable=protected-access - all_path = {"PATH": "/usr/local/bin:/bin/:/usr/sbin/:/usr/local/sbin/"} - with mock.patch.dict('os.environ', all_path): - self.config._path_surgery("thingy") - self.assertEquals(mock_debug.call_count, 0) - self.assertEquals(os.environ["PATH"], all_path["PATH"]) - no_path = {"PATH": "/tmp/"} - with mock.patch.dict('os.environ', no_path): - self.config._path_surgery("thingy") - self.assertEquals(mock_debug.call_count, 1) - self.assertTrue("/usr/local/bin" in os.environ["PATH"]) - self.assertTrue("/tmp" in os.environ["PATH"]) - - @mock.patch("certbot_apache.configurator.ApacheConfigurator.init_augeas") - @mock.patch("certbot_apache.configurator.ApacheConfigurator._path_surgery") - @mock.patch("certbot_apache.configurator.logger.warn") - def test_no_install(self, mock_warn, mock_surgery, _init_augeas): - silly_path = {"PATH": "/tmp/nothingness2342"} - with mock.patch.dict('os.environ', silly_path): - self.assertRaises(errors.NoInstallationError, self.config.prepare) - self.assertEquals(mock_warn.call_count, 1) - self.assertEquals(mock_surgery.call_count, 1) - self.assertTrue("Failed to find" in mock_warn.call_args[0][0]) def test_add_parser_arguments(self): # pylint: disable=no-self-use from certbot_apache.configurator import ApacheConfigurator diff --git a/certbot/plugins/util.py b/certbot/plugins/util.py index 5fc98dff6..cdba88a87 100644 --- a/certbot/plugins/util.py +++ b/certbot/plugins/util.py @@ -1,15 +1,45 @@ """Plugin utilities.""" import logging +import os import socket import psutil import zope.component from certbot import interfaces +from certbot import util logger = logging.getLogger(__name__) +def path_surgery(restart_cmd): + """Attempt to perform PATH surgery to find restart_cmd + + Mitigates https://github.com/certbot/certbot/issues/1833 + + :param str restart_cmd: the command that is being searched for in the PATH + + :returns: True if the operation succeeded, False otherwise + """ + dirs = ("/usr/sbin", "/usr/local/bin", "/usr/local/sbin") + path = os.environ["PATH"] + added = [] + for d in dirs: + if d not in path: + path += os.pathsep + d + added.append(d) + + if any(added): + logger.debug("Can't find %s, attempting PATH mitigation by adding %s", + restart_cmd, os.pathsep.join(added)) + os.environ["PATH"] = path + + if util.exe_exists(restart_cmd): + return True + else: + expanded = " expanded" if any(added) else "" + logger.warn("Failed to find %s in%s PATH: %s", restart_cmd, expanded, path) + return False def already_listening(port, renewer=False): """Check if a process is already listening on the port. diff --git a/certbot/plugins/util_test.py b/certbot/plugins/util_test.py index 9bc8793c7..fa8b364d9 100644 --- a/certbot/plugins/util_test.py +++ b/certbot/plugins/util_test.py @@ -1,9 +1,33 @@ """Tests for certbot.plugins.util.""" +import os import unittest import mock import psutil +class PathSurgeryTest(unittest.TestCase): + """Tests for certbot.plugins.path_surgery.""" + + @mock.patch("certbot.plugins.util.logger.warn") + @mock.patch("certbot.plugins.util.logger.debug") + def test_path_surgery(self, mock_debug, mock_warn): + from certbot.plugins.util import path_surgery + all_path = {"PATH": "/usr/local/bin:/bin/:/usr/sbin/:/usr/local/sbin/"} + with mock.patch.dict('os.environ', all_path): + with mock.patch('certbot.util.exe_exists') as mock_exists: + mock_exists.return_value = True + self.assertEquals(path_surgery("eg"), True) + self.assertEquals(mock_debug.call_count, 0) + self.assertEquals(mock_warn.call_count, 0) + self.assertEquals(os.environ["PATH"], all_path["PATH"]) + no_path = {"PATH": "/tmp/"} + with mock.patch.dict('os.environ', no_path): + path_surgery("thingy") + self.assertEquals(mock_debug.call_count, 1) + self.assertEquals(mock_warn.call_count, 1) + self.assertTrue("Failed to find" in mock_warn.call_args[0][0]) + self.assertTrue("/usr/local/bin" in os.environ["PATH"]) + self.assertTrue("/tmp" in os.environ["PATH"]) class AlreadyListeningTest(unittest.TestCase): """Tests for certbot.plugins.already_listening.""" From ed73c55b7bfca9939d9cd0d8edebddedf29ade2e Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Fri, 8 Jul 2016 16:03:40 -0500 Subject: [PATCH 267/445] Instantiate exception_raised in test. --- certbot/tests/error_handler_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index a32f2a448..2eb1506be 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -42,6 +42,7 @@ class ErrorHandlerTest(unittest.TestCase): self.signals = error_handler._SIGNALS def test_context_manager(self): + exception_raised = False try: with self.handler: raise ValueError From 48b7c01a5925e476f6b4197fa34370cc07d97607 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 8 Jul 2016 14:06:15 -0700 Subject: [PATCH 268/445] bring make_or_verify_dir docstring up to date --- certbot/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot/util.py b/certbot/util.py index 301fc669b..2b40a0f2c 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -95,6 +95,7 @@ def make_or_verify_dir(directory, mode=0o755, uid=0, strict=False): :param str directory: Path to a directory. :param int mode: Directory mode. :param int uid: Directory owner. + :param bool strict: require directory to be owned by current user :raises .errors.Error: if a directory already exists, but has wrong permissions or owner From 894af917778761c2cd3e9143402f71dc1070fe0f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 8 Jul 2016 14:09:26 -0700 Subject: [PATCH 269/445] Fix test --- certbot/plugins/disco_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/certbot/plugins/disco_test.py b/certbot/plugins/disco_test.py index cef6ede8f..75544ee9b 100644 --- a/certbot/plugins/disco_test.py +++ b/certbot/plugins/disco_test.py @@ -55,9 +55,7 @@ class PluginEntryPointTest(unittest.TestCase): name, PluginEntryPoint.entry_point_to_plugin_name(entry_point)) def test_description(self): - self.assertEqual( - "Automatically use a temporary webserver", - self.plugin_ep.description) + self.assertTrue("temporary webserver" in self.plugin_ep.description) def test_description_with_name(self): self.plugin_ep.plugin_cls = mock.MagicMock(description="Desc") From d7772217032235337405f8e4b62a9970f057a8fc Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 8 Jul 2016 14:17:19 -0700 Subject: [PATCH 270/445] write make_or_verify_core_dir --- certbot/main.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index d3f90926a..2a18aa528 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -702,6 +702,23 @@ def _handle_exception(exc_type, exc_value, trace, config): traceback.format_exception(exc_type, exc_value, trace))) +def make_or_verify_core_dir(directory, mode, uid, strict): + """Make sure directory exists with proper permissions. + + :param str directory: Path to a directory. + :param int mode: Directory mode. + :param int uid: Directory owner. + :param bool strict: require directory to be owned by current user + + :raises .errors.Error: if the directory cannot be made or verified + + """ + try: + util.make_or_verify_dir(directory, mode, uid, strict) + except OSError: + raise errors.Error(_PERM_ERR_FMT.format(directory)) + + def main(cli_args=sys.argv[1:]): """Command line argument parsing and main script execution.""" sys.excepthook = functools.partial(_handle_exception, config=None) @@ -712,16 +729,16 @@ def main(cli_args=sys.argv[1:]): config = configuration.NamespaceConfig(args) zope.component.provideUtility(config) - # Setup logging ASAP, otherwise "No handlers could be found for - # logger ..." TODO: this should be done before plugins discovery - for directory in config.config_dir, config.work_dir: - util.make_or_verify_dir( - directory, constants.CONFIG_DIRS_MODE, os.geteuid(), - "--strict-permissions" in cli_args) + make_or_verify_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE, + os.geteuid(), config.strict_permissions) + make_or_verify_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE, + os.geteuid(), config.strict_permissions) # TODO: logs might contain sensitive data such as contents of the # private key! #525 - util.make_or_verify_dir( - config.logs_dir, 0o700, os.geteuid(), "--strict-permissions" in cli_args) + make_or_verify_core_dir(config.logs_dir, 0o700, + os.geteuid(), config.strict_permissions) + # Setup logging ASAP, otherwise "No handlers could be found for + # logger ..." TODO: this should be done before plugins discovery setup_logging(config, _cli_log_handler, logfile='letsencrypt.log') cli.possible_deprecation_warning(config) From e598e907bdbfb69418e6909a5e0f8bc3eb1994e4 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 8 Jul 2016 14:54:51 -0700 Subject: [PATCH 271/445] create MakeOrVerifyCoreDirTest --- certbot/tests/main_test.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 5f6723bd7..32df525f0 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1,4 +1,5 @@ """Tests for certbot.main.""" +import os import shutil import tempfile import unittest @@ -66,5 +67,30 @@ class SetupLogFileHandlerTest(unittest.TestCase): self.config, "test.log", "%s") +class MakeOrVerifyCoreDirTest(unittest.TestCase): + """Tests for certbot.main.make_or_verify_core_dir.""" + + def setUp(self): + self.dir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.dir) + + def _call(self, *args, **kwargs): + from certbot.main import make_or_verify_core_dir + return make_or_verify_core_dir(*args, **kwargs) + + def test_success(self): + new_dir = os.path.join(self.dir, 'new') + self._call(new_dir, 0o700, os.geteuid(), False) + self.assertTrue(os.path.exists(new_dir)) + + @mock.patch('certbot.main.util.make_or_verify_dir') + def test_failure(self, mock_make_or_verify): + mock_make_or_verify.side_effect = OSError + self.assertRaises(errors.Error, self._call, + self.dir, 0o700, os.geteuid(), False) + + if __name__ == '__main__': unittest.main() # pragma: no cover From d4a8820bdcc2cfaf380e58fb026b4681636ed084 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 8 Jul 2016 15:04:44 -0700 Subject: [PATCH 272/445] wrap with escapes --- certbot-apache/certbot_apache/configurator.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 5733baa26..3d6253f33 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -529,7 +529,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if addr.get_port() == "443": is_ssl = True - filename = self._unescape(get_file_path(path)) + filename = get_file_path(self.aug.get("/augeas/files%s/file" % get_file_path(path))) if self.conf("handle-sites"): is_enabled = self.is_site_enabled(filename) else: @@ -770,7 +770,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ avail_fp = nonssl_vhost.filep - ssl_fp = self._escape(self._get_ssl_vhost_path(avail_fp)) + ssl_fp = self._get_ssl_vhost_path(avail_fp) self._copy_create_ssl_vhost_skeleton(avail_fp, ssl_fp) @@ -778,7 +778,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.aug.load() # Get Vhost augeas path for new vhost vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" % - (ssl_fp, parser.case_i("VirtualHost"))) + (_escape(ssl_fp), parser.case_i("VirtualHost"))) if len(vh_p) != 1: logger.error("Error: should only be one vhost in %s", avail_fp) raise errors.PluginError("Currently, we only support " @@ -997,11 +997,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if need_to_save: self.save() - def _unescape(self, fp): - return fp.replace("\\", "") - def _escape(self, fp): - return fp.replace(",", "\\,") + fp = fp.replace(",", "\\,") + fp = fp.replace("[", "\\[") + fp = fp.replace("]", "\\]") + fp = fp.replace("|", "\\|") + fp = fp.replace("=", "\\=") + fp = fp.replace("(", "\\(") + fp = fp.replace(")", "\\)") + fp = fp.replace("!", "\\!") ###################################################################### # Enhancements @@ -1332,7 +1336,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.aug.load() # Make a new vhost data structure and add it to the lists - new_vhost = self._create_vhost(parser.get_aug_path(redirect_filepath)) + new_vhost = self._create_vhost(parser.get_aug_path(self._escape(redirect_filepath))) self.vhosts.append(new_vhost) self._enhanced_vhosts["redirect"].add(new_vhost) From 1113e280460f811486a6404d4ed5a9b0c286ef8f Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 8 Jul 2016 15:22:56 -0700 Subject: [PATCH 273/445] fix typo --- certbot-apache/certbot_apache/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 3d6253f33..afaf7f93e 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -529,7 +529,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if addr.get_port() == "443": is_ssl = True - filename = get_file_path(self.aug.get("/augeas/files%s/file" % get_file_path(path))) + filename = get_file_path(self.aug.get("/augeas/files%s/path" % get_file_path(path))) if self.conf("handle-sites"): is_enabled = self.is_site_enabled(filename) else: From d8c2dd1a5c741bb9d2f375bf3f453a44a6ab7184 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 8 Jul 2016 15:28:12 -0700 Subject: [PATCH 274/445] add self --- certbot-apache/certbot_apache/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index afaf7f93e..a2a4182db 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -778,7 +778,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.aug.load() # Get Vhost augeas path for new vhost vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" % - (_escape(ssl_fp), parser.case_i("VirtualHost"))) + (self._escape(ssl_fp), parser.case_i("VirtualHost"))) if len(vh_p) != 1: logger.error("Error: should only be one vhost in %s", avail_fp) raise errors.PluginError("Currently, we only support " From 1bbfde1771a97b828218061b1c83734a087896ce Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 8 Jul 2016 15:35:29 -0700 Subject: [PATCH 275/445] don't code while distracted --- certbot-apache/certbot_apache/configurator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index a2a4182db..b4ce29d31 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -1006,6 +1006,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): fp = fp.replace("(", "\\(") fp = fp.replace(")", "\\)") fp = fp.replace("!", "\\!") + return fp ###################################################################### # Enhancements From 9372914c67e189c00a4f6c4143011811b4a617d9 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 8 Jul 2016 15:51:31 -0700 Subject: [PATCH 276/445] Improve error message --- certbot/main.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/certbot/main.py b/certbot/main.py index 2a18aa528..8bccc524d 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -36,9 +36,10 @@ from certbot.plugins import disco as plugins_disco from certbot.plugins import selection as plug_sel -_PERM_ERR_FMT = ("An error occurred while trying to create or modify {0}. To " - "run as non-root, set --config-dir, --logs-dir, and " - "--work-dir to writeable paths.") +_PERM_ERR_FMT = os.linesep.join(( + "The following error was encountered:", "{0}", + "If running as non-root, set --config-dir, " + "--logs-dir, and --work-dir to writeable paths.")) logger = logging.getLogger(__name__) @@ -601,8 +602,8 @@ def setup_log_file_handler(config, logfile, fmt): try: handler = logging.handlers.RotatingFileHandler( log_file_path, maxBytes=2 ** 20, backupCount=10) - except IOError: - raise errors.Error(_PERM_ERR_FMT.format(log_file_path)) + except IOError as error: + raise errors.Error(_PERM_ERR_FMT.format(error)) # rotate on each invocation, rollover only possible when maxBytes # is nonzero and backupCount is nonzero, so we set maxBytes as big # as possible not to overrun in single CLI invocation (1MB). @@ -715,8 +716,8 @@ def make_or_verify_core_dir(directory, mode, uid, strict): """ try: util.make_or_verify_dir(directory, mode, uid, strict) - except OSError: - raise errors.Error(_PERM_ERR_FMT.format(directory)) + except OSError as error: + raise errors.Error(_PERM_ERR_FMT.format(error)) def main(cli_args=sys.argv[1:]): From 64012a6053dd97b1832ed241929df36d8963e8db Mon Sep 17 00:00:00 2001 From: Jacob Sachs Date: Wed, 15 Jun 2016 13:23:52 -0400 Subject: [PATCH 277/445] set dialog widgets to use autowidgetsize --- certbot/display/util.py | 21 +++++++-------------- certbot/tests/display/util_test.py | 7 ++----- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/certbot/display/util.py b/certbot/display/util.py index 39486b2bd..258afd948 100644 --- a/certbot/display/util.py +++ b/certbot/display/util.py @@ -82,7 +82,7 @@ class NcursesDisplay(object): def __init__(self, width=WIDTH, height=HEIGHT): super(NcursesDisplay, self).__init__() - self.dialog = dialog.Dialog() + self.dialog = dialog.Dialog(autowidgetsize=True) assert OK == self.dialog.DIALOG_OK, "What kind of absurdity is this?" self.width = width self.height = height @@ -101,7 +101,7 @@ class NcursesDisplay(object): :param bool pause: Not applicable to NcursesDisplay """ - self.dialog.msgbox(message, height, width=self.width) + self.dialog.msgbox(message) def menu(self, message, choices, ok_label="OK", cancel_label="Cancel", help_label="", **unused_kwargs): @@ -170,11 +170,7 @@ class NcursesDisplay(object): `string` - input entered by the user """ - sections = message.split("\n") - # each section takes at least one line, plus extras if it's longer than self.width - wordlines = [1 + (len(section) / self.width) for section in sections] - height = 6 + sum(wordlines) + len(sections) - return _clean(self.dialog.inputbox(message, width=self.width, height=height)) + return self.dialog.inputbox(message) def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs): """Display a Yes/No dialog box. @@ -191,8 +187,7 @@ class NcursesDisplay(object): """ return self.dialog.DIALOG_OK == self.dialog.yesno( - message, self.height, self.width, - yes_label=yes_label, no_label=no_label) + message, yes_label=yes_label, no_label=no_label) def checklist(self, message, tags, default_status=True, **unused_kwargs): """Displays a checklist. @@ -210,8 +205,7 @@ class NcursesDisplay(object): """ choices = [(tag, "", default_status) for tag in tags] - return _clean(self.dialog.checklist( - message, width=self.width, height=self.height, choices=choices)) + return self.dialog.checklist(message, choices=choices) def directory_select(self, message, **unused_kwargs): """Display a directory selection screen. @@ -224,9 +218,8 @@ class NcursesDisplay(object): """ root_directory = os.path.abspath(os.sep) - return _clean(self.dialog.dselect( - filepath=root_directory, width=self.width, - height=self.height, help_button=True, title=message)) + return self.dialog.dselect( + filepath=root_directory, help_button=True, title=message) @zope.interface.implementer(interfaces.IDisplay) diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index a6ced90ab..00ee6be36 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -107,8 +107,7 @@ class NcursesDisplayTest(unittest.TestCase): self.assertTrue(self.displayer.yesno("message")) mock_yesno.assert_called_with( - "message", display_util.HEIGHT, display_util.WIDTH, - yes_label="Yes", no_label="No") + "message", yes_label="Yes", no_label="No") @mock.patch("certbot.display.util." "dialog.Dialog.checklist") @@ -121,9 +120,7 @@ class NcursesDisplayTest(unittest.TestCase): (TAGS[1], "", True), (TAGS[2], "", True), ] - mock_checklist.assert_called_with( - "message", width=display_util.WIDTH, height=display_util.HEIGHT, - choices=choices) + mock_checklist.assert_called_with("message", choices=choices) @mock.patch("certbot.display.util.dialog.Dialog.dselect") def test_directory_select(self, mock_dselect): From 156ea46a09fde04fd24c959566d25a11326a089d Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Fri, 8 Jul 2016 16:30:50 -0500 Subject: [PATCH 278/445] Compare initial and final signals in tests. --- certbot/tests/error_handler_test.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index 2eb1506be..2e2ffe2d9 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -7,18 +7,25 @@ import unittest import mock +def get_signals(signums): + """Get the handlers for an iterable of signums.""" + return dict((s, signal.getsignal(s)) for s in signums) + + +def set_signals(sig_handler_dict): + """Set the signal (keys) with the handler (values) from the input dict.""" + tuple(signal.signal(s, h) for (s, h) in sig_handler_dict.items()) + @contextlib.contextmanager def signal_receiver(signums): """Context manager to catch signals""" signals = [] prev_handlers = {} - for signum in signums: - prev_handlers[signum] = signal.getsignal(signum) - signal.signal(signum, lambda signum, _: signals.append(signum)) + prev_handlers = get_signals(signums) + set_signals(dict((s, lambda s, _: signals.append(s)) for s in signums)) yield signals - for signum in signums: - signal.signal(signum, prev_handlers[signum]) + set_signals(dict((s, prev_handlers[s]) for s in signums)) def send_signal(signum): @@ -54,6 +61,7 @@ class ErrorHandlerTest(unittest.TestCase): **self.init_kwargs) def test_context_manager_with_signal(self): + init_signals = get_signals(self.signals) with signal_receiver(self.signals) as signals_received: with self.handler: should_be_42 = 42 @@ -68,8 +76,7 @@ class ErrorHandlerTest(unittest.TestCase): self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) for signum in self.signals: - sig = signal.getsignal(signum) - self.assertTrue((sig == signal.SIG_DFL) or (sig == signal.SIG_IGN)) + self.assertEqual(init_signals[signum], signal.getsignal(signum)) def test_bad_recovery(self): bad_func = mock.MagicMock(side_effect=[ValueError]) From 68500cd4361bd0d07167c8f42a77adec3ac034f9 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 9 Jul 2016 15:13:09 -0700 Subject: [PATCH 279/445] Don't allow dollar_var to swalllow characters like "{" --- certbot-nginx/certbot_nginx/nginxparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index 0cc912515..1859777d8 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -23,7 +23,7 @@ class RawNginxParser(object): right_bracket = space.leaveWhitespace() + Literal("}").suppress() semicolon = Literal(";").suppress() key = Word(alphanums + "_/+-.") - dollar_var = Combine(Literal('$') + nonspace) + dollar_var = Combine(Literal('$') + Regex(r"[^\{\};,\s]+")) condition = Regex(r"\(.+\)") # Matches anything that is not a special character AND any chars in single # or double quotes From 9bc50d4a4761e6734389697b1cdff1c5339726d7 Mon Sep 17 00:00:00 2001 From: Blake Griffith Date: Mon, 11 Jul 2016 12:43:33 -0500 Subject: [PATCH 280/445] Try to fix travis-ci lint failure --- certbot-apache/certbot_apache/tests/configurator_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 633757de4..99b1b8b74 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -1,4 +1,4 @@ -# pylint: disable=too-many-public-methods +# pylint: disable=too-many-public-methods,too-many-lines """Test for certbot_apache.configurator.""" import os import shutil From 8f1a141d2a3c343d1521a48897fde187e7bcc94e Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Mon, 11 Jul 2016 13:20:31 -0700 Subject: [PATCH 281/445] incorporate brad's comments --- certbot-apache/certbot_apache/configurator.py | 2 +- certbot-apache/certbot_apache/tests/configurator_test.py | 3 ++- .../augeas_vhosts/apache2/sites-enabled/000-default.conf | 1 - .../augeas_vhosts/apache2/sites-enabled/certbot.conf | 1 - .../apache2/sites-enabled/encryption-example.conf | 1 - .../augeas_vhosts/apache2/sites-enabled/mod_macro-example.conf | 1 - .../augeas_vhosts/apache2/sites-enabled/ocsp-ssl.conf | 1 - 7 files changed, 3 insertions(+), 7 deletions(-) delete mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/000-default.conf delete mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/certbot.conf delete mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/encryption-example.conf delete mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/mod_macro-example.conf delete mode 120000 certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/ocsp-ssl.conf diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index b4ce29d31..2c9bd982f 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -1080,7 +1080,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not use_stapling_aug_path: self.parser.add_dir(ssl_vhost.path, "SSLUseStapling", "on") - ssl_vhost_aug_path = parser.get_aug_path(ssl_vhost.filep) + ssl_vhost_aug_path = self._escape(parser.get_aug_path(ssl_vhost.filep)) # Check if there's an existing SSLStaplingCache directive. stapling_cache_aug_path = self.parser.find_dir('SSLStaplingCache', diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index a90940f94..ceef5b9e4 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -1,4 +1,4 @@ -# pylint: disable=too-many-public-methods, protected-access +# pylint: disable=too-many-public-methods """Test for certbot_apache.configurator.""" import os import shutil @@ -1188,6 +1188,7 @@ class MultipleVhostsTest(util.ApacheTest): class AugeasVhostsTest(util.ApacheTest): """Test vhosts with illegal names dependant on augeas version.""" + # pylint: disable=protected-access def setUp(self): # pylint: disable=arguments-differ td = "debian_apache_2_4/augeas_vhosts" diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/000-default.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/000-default.conf deleted file mode 120000 index 3c4632b73..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/000-default.conf +++ /dev/null @@ -1 +0,0 @@ -../sites-available/000-default.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/certbot.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/certbot.conf deleted file mode 120000 index 4d08c763f..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/certbot.conf +++ /dev/null @@ -1 +0,0 @@ -../sites-available/certbot.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/encryption-example.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/encryption-example.conf deleted file mode 120000 index 417818069..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/encryption-example.conf +++ /dev/null @@ -1 +0,0 @@ -../sites-available/encryption-example.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/mod_macro-example.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/mod_macro-example.conf deleted file mode 120000 index 44f254304..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/mod_macro-example.conf +++ /dev/null @@ -1 +0,0 @@ -../sites-available/mod_macro-example.conf \ No newline at end of file diff --git a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/ocsp-ssl.conf b/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/ocsp-ssl.conf deleted file mode 120000 index b25ee0482..000000000 --- a/certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-enabled/ocsp-ssl.conf +++ /dev/null @@ -1 +0,0 @@ -../sites-available/ocsp-ssl.conf \ No newline at end of file From b48ddac5285572129937661e0e8291a329d3bb98 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 11 Jul 2016 13:58:21 -0700 Subject: [PATCH 282/445] Initial version of nginx parser roundtrip test --- certbot-compatibility-test/nginx/README | 27 ++++ .../79-configs/site-10033 | 34 +++++ .../79-configs/site-10571 | 71 +++++++++ .../79-configs/site-10591 | 38 +++++ .../79-configs/site-10920 | 16 +++ .../79-configs/site-10947 | 40 ++++++ .../79-configs/site-11018 | 37 +++++ .../79-configs/site-11046 | 36 +++++ .../79-configs/site-11382 | 29 ++++ .../79-configs/site-1167 | 38 +++++ .../79-configs/site-11849 | 36 +++++ .../79-configs/site-12027 | 29 ++++ .../79-configs/site-12235 | 33 +++++ .../79-configs/site-12649 | 45 ++++++ .../79-configs/site-13577 | 38 +++++ .../79-configs/site-14402 | 33 +++++ .../79-configs/site-14430 | 54 +++++++ .../79-configs/site-15141 | 36 +++++ .../79-configs/site-15270 | 38 +++++ .../79-configs/site-15291 | 112 +++++++++++++++ .../79-configs/site-15456 | 39 +++++ .../79-configs/site-15497 | 35 +++++ .../79-configs/site-15852 | 38 +++++ .../79-configs/site-16345 | 34 +++++ .../79-configs/site-17175 | 14 ++ .../79-configs/site-17832 | 32 +++++ .../79-configs/site-17942 | 32 +++++ .../79-configs/site-18018 | 36 +++++ .../79-configs/site-18069 | 39 +++++ .../79-configs/site-19334 | 39 +++++ .../79-configs/site-19639 | 39 +++++ .../79-configs/site-1966 | 36 +++++ .../79-configs/site-19791 | 34 +++++ .../79-configs/site-19955 | 36 +++++ .../79-configs/site-21369 | 33 +++++ .../79-configs/site-21549 | 32 +++++ .../79-configs/site-230 | 33 +++++ .../79-configs/site-23325 | 74 ++++++++++ .../79-configs/site-23470 | 56 ++++++++ .../79-configs/site-23791 | 33 +++++ .../79-configs/site-23803 | 32 +++++ .../79-configs/site-23838 | 32 +++++ .../79-configs/site-24125 | 7 + .../79-configs/site-24193 | 62 ++++++++ .../79-configs/site-24213 | 36 +++++ .../79-configs/site-25480 | 32 +++++ .../79-configs/site-26195 | 26 ++++ .../79-configs/site-26221 | 32 +++++ .../79-configs/site-26637 | 32 +++++ .../79-configs/site-26758 | 21 +++ .../79-configs/site-27646 | 37 +++++ .../79-configs/site-27728 | 5 + .../79-configs/site-27736 | 32 +++++ .../79-configs/site-27812 | 36 +++++ .../79-configs/site-28050 | 36 +++++ .../79-configs/site-28690 | 32 +++++ .../79-configs/site-29159 | 33 +++++ .../79-configs/site-2951 | 67 +++++++++ .../79-configs/site-30011 | 37 +++++ .../79-configs/site-30571 | 31 ++++ .../79-configs/site-31900 | 33 +++++ .../79-configs/site-32190 | 4 + .../79-configs/site-32279 | 25 ++++ .../79-configs/site-32317 | 32 +++++ .../79-configs/site-32438 | 46 ++++++ .../79-configs/site-3483 | 32 +++++ .../79-configs/site-3507 | 44 ++++++ .../79-configs/site-3874 | 46 ++++++ .../79-configs/site-4035 | 31 ++++ .../79-configs/site-4143 | 33 +++++ .../79-configs/site-4264 | 12 ++ .../79-configs/site-5826 | 38 +++++ .../79-configs/site-5872 | 36 +++++ .../79-configs/site-6228 | 39 +++++ .../79-configs/site-7895 | 32 +++++ .../79-configs/site-8343 | 36 +++++ .../79-configs/site-8422 | 46 ++++++ .../79-configs/site-8637 | 40 ++++++ .../79-configs/site-8662 | 32 +++++ .../79-configs/site-9426 | 111 ++++++++++++++ .../activecolab/www.example.com.vhost | 44 ++++++ .../chive/chive-nginx-master/fastcgi.conf | 9 ++ .../chive/chive-nginx-master/fastcgi_params | 32 +++++ .../chive/chive-nginx-master/koi-utf | 109 ++++++++++++++ .../chive/chive-nginx-master/koi-win | 103 +++++++++++++ .../chive-nginx-master/map_https_fcgi.conf | 7 + .../chive/chive-nginx-master/mime.types | 77 ++++++++++ .../chive/chive-nginx-master/nginx.conf | 119 +++++++++++++++ .../chive-nginx-master/reverse_proxy.conf | 10 ++ .../sites-available/000-default | 19 +++ .../sites-available/chive.example.com.conf | 102 +++++++++++++ .../secure.chive.example.com.conf | 135 ++++++++++++++++++ .../upstream_phpapache.conf | 8 ++ .../chive-nginx-master/upstream_phpcgi.conf | 8 ++ .../chive/chive-nginx-master/win-utf | 126 ++++++++++++++++ .../cms-made-simple/nginx.conf | 17 +++ .../codeigniter/nginx-alt.conf | 25 ++++ .../codeigniter/nginx.conf | 22 +++ .../contao/sites-available/example.com.vhost | 41 ++++++ .../cs-cart/sites-available/example.com.vhost | 65 +++++++++ .../djangofastcgi/large.conf | 98 +++++++++++++ .../djangofastcgi/nginx.conf | 34 +++++ .../dokuwiki/dokuwiki.conf | 30 ++++ .../dokuwiki/drop.conf | 4 + .../dokuwiki/full.conf | 61 ++++++++ .../dokuwiki/nginx-no-ssl.conf | 29 ++++ .../dokuwiki/nginx.conf | 30 ++++ .../drupal/nginx.conf | 95 ++++++++++++ .../dynamic_ssi/nginx.conf | 39 +++++ .../nginx-roundtrip-testdata/elgg/nginx.conf | 84 +++++++++++ .../embeddedperlminifyjs/nginx.conf | 19 +++ .../embeddedperlsitemapsproxy/nginx.conf | 29 ++++ .../expressionengine/bad.conf | 24 ++++ .../expressionengine/better.conf | 24 ++++ .../expressionengine/yourpath.conf | 37 +++++ .../fastcgiexample/fastcgi.conf | 18 +++ .../fastcgiexample/nginx.conf | 6 + .../sites-available/www.example.com.vhost | 33 +++++ .../full-example/fastcgi.conf | 21 +++ .../full-example/mime.types | 48 +++++++ .../full-example/nginx.conf | 70 +++++++++ .../full-example/proxy.conf | 10 ++ .../fullexample2/nginx.conf | 126 ++++++++++++++++ .../nginx-roundtrip-testdata/geoip/nginx.conf | 9 ++ .../guide-to-nginx-ssl-spdy-hsts/nginx.conf | 120 ++++++++++++++++ .../hardwarelberrors/nginx.conf | 22 +++ .../sites-available/www.example.com.vhost | 66 +++++++++ .../nginx.conf | 39 +++++ .../nginx.conf | 27 ++++ .../imapproxyexample/nginx.conf | 38 +++++ .../imapproxyexample/proxy-example.conf | 20 +++ .../iphone-website-with-nginx/mobile.conf | 37 +++++ .../iphone-website-with-nginx/nginx.conf | 33 +++++ .../iredmail/iredadmin.conf | 31 ++++ .../iredmail/nginx.conf | 43 ++++++ .../javaservers/nginx.conf | 49 +++++++ .../joomla/nginx.conf | 39 +++++ .../likeapache/nginx.conf | 11 ++ .../loadbalanceexample/nginx.conf | 16 +++ .../mailman/nginx.conf | 37 +++++ .../mediawiki/nginx.conf | 44 ++++++ .../memcachepreload/sites-available/default | 12 ++ .../minio/sites-enabled/nginx.conf | 10 ++ .../nginx-roundtrip-testdata/mono/nginx.conf | 36 +++++ .../nginx-roundtrip-testdata/mybb/nginx.conf | 27 ++++ .../nonrootwebpath/nginx.conf | 7 + .../nginx-roundtrip-testdata/omeka/nginx.conf | 50 +++++++ .../oscommerce/nginx.conf | 50 +++++++ .../osticket/nginx.conf | 71 +++++++++ .../sites-available/www.example.com.vhost | 75 ++++++++++ .../sites-available/www.example.com.vhost | 66 +++++++++ .../php-fpm/default.conf | 9 ++ .../phpbb/nginx.sample.conf | 129 +++++++++++++++++ .../phpfastcgionwindows/nginx.conf | 8 ++ .../phpfcgi/fastcgi_params | 27 ++++ .../phpfcgi/nginx.conf | 10 ++ .../phplist/nginx.conf | 44 ++++++ .../nginx-roundtrip-testdata/piwik/nginx.conf | 70 +++++++++ .../pmwiki/nginx.conf | 39 +++++ .../sites-available/www.example.com.vhost | 75 ++++++++++ .../sites-available/www.example.com.vhost | 64 +++++++++ .../pylons/nginx.vhost.conf | 11 ++ .../pyrocms/drop.conf | 4 + .../pyrocms/fastcgi_params | 31 ++++ .../pyrocms/nginx.conf | 50 +++++++ .../qwebric/redirect.conf | 6 + .../qwebric/reverse-proxy.conf | 18 +++ .../sites-available/www.example.com.vhost | 46 ++++++ .../redmine/nginx.conf | 19 +++ .../reverseproxycachingexample/nginx.conf | 14 ++ .../sites-available/example.com.vhost.conf | 46 ++++++ .../nginx.conf | 20 +++ .../server_blocks/catchall.conf | 13 ++ .../server_blocks/two.conf | 17 +++ .../server_blocks/wildcard-subdomains.conf | 31 ++++ .../sites-available/www.example.com.vhost | 75 ++++++++++ .../sites-available/www.example.com.vhost | 53 +++++++ .../silverstripe/nginx.conf | 72 ++++++++++ .../simplecgi/nginx.conf | 26 ++++ .../sites-available/www.example.com.vhost | 78 ++++++++++ .../simplepythonfcgi/fastcgi.conf | 20 +++ .../simplepythonfcgi/nginx.conf | 17 +++ .../simplerubyfcgi/nginx.conf | 32 +++++ .../nginx-roundtrip-testdata/spip/nginx.conf | 24 ++++ .../sites-available/www.example.com.vhost | 39 +++++ .../symfony/nginx.conf | 54 +++++++ .../nginx-roundtrip-testdata/symfony/old.conf | 70 +++++++++ .../symfony/oldold.conf | 50 +++++++ .../sites-available/www.example.com.vhost | 89 ++++++++++++ .../sites-available/www.example.com.vhost | 91 ++++++++++++ .../wordpress-caching/no-cache.conf | 41 ++++++ .../wordpress-caching/supercache.conf | 74 ++++++++++ .../wordpress-caching/total-cache.conf | 41 ++++++ .../totalcache-enhanced.conf | 64 +++++++++ .../wordpress/multisite-subdir.conf | 47 ++++++ .../wordpress/multisite-subdomain.conf | 39 +++++ .../wordpress/nginx.conf | 43 ++++++ .../xenforo/nginx.conf | 18 +++ .../nginx-roundtrip-testdata/yii/nginx.conf | 42 ++++++ .../nginx-roundtrip-testdata/zend/nginx.conf | 16 +++ .../zenphoto/nginx.conf | 93 ++++++++++++ .../nginx-roundtrip-testdata/zope/nginx.conf | 18 +++ certbot-compatibility-test/nginx/roundtrip.py | 34 +++++ 203 files changed, 8263 insertions(+) create mode 100644 certbot-compatibility-test/nginx/README create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10033 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10571 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10591 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10920 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10947 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11018 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11046 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11382 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-1167 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11849 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12027 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12235 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12649 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-13577 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-14402 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-14430 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15141 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15270 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15291 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15456 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15497 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15852 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-16345 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17175 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17832 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17942 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-18018 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-18069 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19334 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19639 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-1966 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19791 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19955 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-21369 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-21549 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-230 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23325 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23470 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23791 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23803 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23838 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24125 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24193 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24213 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-25480 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26195 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26221 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26637 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26758 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27646 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27728 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27736 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27812 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-28050 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-28690 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-29159 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-2951 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-30011 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-30571 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-31900 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32190 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32279 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32317 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32438 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3483 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3507 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3874 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4035 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4143 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4264 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-5826 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-5872 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-6228 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-7895 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8343 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8422 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8637 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8662 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-9426 create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/activecolab/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/fastcgi.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/fastcgi_params create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/koi-utf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/koi-win create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/map_https_fcgi.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/mime.types create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/reverse_proxy.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/000-default create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/chive.example.com.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/secure.chive.example.com.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/upstream_phpapache.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/upstream_phpcgi.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/win-utf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/cms-made-simple/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/codeigniter/nginx-alt.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/codeigniter/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/contao/sites-available/example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/cs-cart/sites-available/example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/djangofastcgi/large.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/djangofastcgi/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dokuwiki/dokuwiki.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dokuwiki/drop.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dokuwiki/full.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dokuwiki/nginx-no-ssl.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dokuwiki/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/drupal/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dynamic_ssi/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/elgg/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/embeddedperlminifyjs/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/embeddedperlsitemapsproxy/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/expressionengine/bad.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/expressionengine/better.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/expressionengine/yourpath.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/fastcgiexample/fastcgi.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/fastcgiexample/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/fengoffice/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/full-example/fastcgi.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/full-example/mime.types create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/full-example/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/full-example/proxy.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/fullexample2/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/geoip/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/guide-to-nginx-ssl-spdy-hsts/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/hardwarelberrors/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/icinga/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/imapauthenticatewithapacheperlscript/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/imapauthenticatewithapachephpscript/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/imapproxyexample/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/imapproxyexample/proxy-example.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/iphone-website-with-nginx/mobile.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/iphone-website-with-nginx/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/iredmail/iredadmin.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/iredmail/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/javaservers/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/joomla/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/likeapache/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/loadbalanceexample/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/mailman/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/mediawiki/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/memcachepreload/sites-available/default create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/minio/sites-enabled/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/mono/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/mybb/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/nonrootwebpath/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/omeka/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/oscommerce/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/osticket/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/owncloud/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/oxid-eshop/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/php-fpm/default.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phpbb/nginx.sample.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phpfastcgionwindows/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phpfcgi/fastcgi_params create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phpfcgi/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phplist/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/piwik/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/pmwiki/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/prestashop/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/processwire/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/pylons/nginx.vhost.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/pyrocms/drop.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/pyrocms/fastcgi_params create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/pyrocms/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/qwebric/redirect.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/qwebric/reverse-proxy.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/redaxo/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/redmine/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/reverseproxycachingexample/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/roundcube/sites-available/example.com.vhost.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/separateerrorloggingpervirtualhost/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/server_blocks/catchall.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/server_blocks/two.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/server_blocks/wildcard-subdomains.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/shopware/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/shopware4/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/silverstripe/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/simplecgi/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/simplegroupware/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/simplepythonfcgi/fastcgi.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/simplepythonfcgi/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/simplerubyfcgi/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/spip/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/sugarcrm/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/symfony/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/symfony/old.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/symfony/oldold.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/typo3-4.6/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/typo3-6.2/sites-available/www.example.com.vhost create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress-caching/no-cache.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress-caching/supercache.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress-caching/total-cache.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress-caching/totalcache-enhanced.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress/multisite-subdir.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress/multisite-subdomain.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/xenforo/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/yii/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/zend/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/zenphoto/nginx.conf create mode 100644 certbot-compatibility-test/nginx/nginx-roundtrip-testdata/zope/nginx.conf create mode 100644 certbot-compatibility-test/nginx/roundtrip.py diff --git a/certbot-compatibility-test/nginx/README b/certbot-compatibility-test/nginx/README new file mode 100644 index 000000000..f32de2148 --- /dev/null +++ b/certbot-compatibility-test/nginx/README @@ -0,0 +1,27 @@ +Eventually there will also be a compatibility test here like the Apache one. + +Right now, this is data for the roundtrip test (checking that the parser +can parse each file and that the reserialized config file it generates is +identical to the original). + +If run in a virtualenv or otherwise so that certbot_nginx can be imported, +the roundtrip test can run as + +python roundtrip.py nginx-roundtrip-testdata + +It gives exit status 0 for success and 1 if at least one parse or roundtrip +failure occurred. + + +The directory nginx-roundtrip-testdata includes some config files that were +contributed to our project as well as most of the configs linked from + +https://www.nginx.com/resources/wiki/start/ + +Some exceptions that were skipped are + +https://www.nginx.com/resources/wiki/start/topics/recipes/moinmoin/ +https://www.nginx.com/resources/wiki/start/topics/examples/SSL-Offloader/ (not much nginx configuration) +https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/ (likewise) +https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/ +https://www.nginx.com/resources/wiki/start/topics/examples/fcgiwrap/ diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10033 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10033 new file mode 100644 index 000000000..19dc49444 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10033 @@ -0,0 +1,34 @@ +upstream django_server_random18709.example.org { + server unix:/srv/http/random22194/live/website.sock; +} + +server { + listen 80; + server_name random18709.example.org; + + location /media/ { + alias /srv/http/random22194/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random22194/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random18709.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random22194/live/access.log combined_plus; + error_log /var/log/nginx/random22194/live/error.log; +} + +server { + server_name www.random18709.example.org; + server_name random24607.example.org www.random24607.example.org; + return 301 http://random18709.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10571 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10571 new file mode 100644 index 000000000..fe95ac8dc --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10571 @@ -0,0 +1,71 @@ +upstream django_server_random1413.example.org { + server unix:/srv/http/random25151/live/website.sock; +} + +server { + listen 443; + server_name www.random25266.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random25266.example.org.bundle.crt; + ssl_certificate_key /etc/ssl/private/random25266.example.org.key; + + location /media/ { + alias /srv/http/random25151/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random25151/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random1413.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random25151/live/access.log combined_plus; + error_log /var/log/nginx/random25151/live/error.log; +} + + +server { + listen 443; + server_name random1413.example.org www.random1413.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random1413.example.org.bundle.crt; + ssl_certificate_key /etc/ssl/private/random1413.example.org.key; + + location / { + return 301 https://www.random25266.example.org$request_uri; + } +} + +server { + listen 443; + server_name random25266.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random25266.example.org.bundle.crt; + ssl_certificate_key /etc/ssl/private/random25266.example.org.key; + + location / { + return 301 https://www.random25266.example.org$request_uri; + } +} + +server { + listen 80; + server_name random1413.example.org www.random1413.example.org; + server_name random28524.example.org www.random28524.example.org; + server_name random25266.example.org www.random25266.example.org; + server_name random26791.example.org www.random26791.example.org; + + location / { + return 301 https://www.random25266.example.org$request_uri; + } +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10591 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10591 new file mode 100644 index 000000000..103b56009 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10591 @@ -0,0 +1,38 @@ +upstream django_server_random11921.example.org { + server unix:/srv/http/random9726/acceptance/website.sock; +} + +server { + listen 80; + server_name random11921.example.org www.random11921.example.org; + + if ($host != 'random11921.example.org') { + rewrite ^/(.*)$ http://random11921.example.org/$1 permanent; + } + + location /media/ { + alias /srv/http/random9726/acceptance/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random9726/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random11921.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + error_page 502 503 504 /50x.html; + } + + location /50x.html { + root /usr/share/nginx/www/; + } + + access_log /var/log/nginx/random9726/acceptance/access.log combined_plus; + error_log /var/log/nginx/random9726/acceptance/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10920 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10920 new file mode 100644 index 000000000..0f7c55762 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10920 @@ -0,0 +1,16 @@ +server { + listen 80 default; + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $host; + proxy_pass http://127.0.0.1:81; + } + + location ~ /\.ht { + deny all; + } + + access_log /var/log/nginx/random27802/access.log combined_plus; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10947 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10947 new file mode 100644 index 000000000..a09605d03 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10947 @@ -0,0 +1,40 @@ +upstream django_server_acceptance.random8289.random17507.example.org { + server unix:/srv/http/random8289/acceptance/website.sock; +} + +server { + listen 80; + server_name random23045.example.org; + + location /media/ { + alias /srv/http/random8289/acceptance/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random8289/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_acceptance.random8289.random17507.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + + satisfy any; + auth_basic 'random8289 acceptance'; + auth_basic_user_file /srv/http/random8289/acceptance/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random8289/acceptance/access.log combined_plus; + error_log /var/log/nginx/random8289/acceptance/error.log; +} + +server { + server_name www.random23045.example.org; + return 301 http://random23045.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11018 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11018 new file mode 100644 index 000000000..8aceca7ca --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11018 @@ -0,0 +1,37 @@ +upstream django_server_random24036.example.org { + server unix:/srv/http/random1006/live/website.sock; +} + +server { + listen 80; + server_name random24036.example.org; + gzip on; + gzip_http_version 1.0; + gzip_types *; + gzip_vary on; + gzip_proxied any; + + location ~ /media/(.*)$ { + alias /srv/http/random1006/live/website/static/$1; + expires 7d; + gzip on; + } + + + location / { + proxy_pass http://django_server_random24036.example.org; + include /etc/nginx/proxy_params; + + # You can configure access rules here + } + + access_log /var/log/nginx/random1006/live/access.log combined_plus; + error_log /var/log/nginx/random1006/live/error.log; +} + +server { + server_name www.random24036.example.org; + server_name random32349.example.org www.random32349.example.org; + server_name random23794.example.org www.random23794.example.org; + rewrite ^ http://random24036.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11046 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11046 new file mode 100644 index 000000000..1d81e5b52 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11046 @@ -0,0 +1,36 @@ +upstream django_server_random25979.example.org { + server unix:/srv/http/random24211/internal/website.sock; +} + +server { + listen 80; + server_name random25979.example.org; + + location ^~ /media/ { + alias /srv/http/random24211/internal/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random24211/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random25979.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'internal for random24211'; + auth_basic_user_file /srv/http/random24211/internal/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random24211/internal/access.log combined_plus; + error_log /var/log/nginx/random24211/internal/error.log; +} + +server { + server_name www.random25979.example.org; + rewrite ^ http://intern.random24211.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11382 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11382 new file mode 100644 index 000000000..0dc1af725 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11382 @@ -0,0 +1,29 @@ +server { + listen 80; + listen 7891; # User0 + listen 8080; # User1 + listen 8900; # User2 + listen 8912; # User3 + listen 3567; # User4 + + server_name random666.example.org www.random666.example.org; + + root /srv/http/random666.example.org; + index index.html index.htm; + + location /duif_assets/ { + try_files $uri $uri/ =404; + } + + location /index.html { + try_files $uri $uri/ =404; + } + + location / { + rewrite ^.+$ / break; + try_files $uri $uri/ =404; + } + + access_log /var/log/nginx/random666.example.org/access.log combined_plus; + error_log /var/log/nginx/random666.example.org/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-1167 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-1167 new file mode 100644 index 000000000..13210b056 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-1167 @@ -0,0 +1,38 @@ +upstream django_server_random23900.example.org { + server unix:/srv/http/random29467/acceptance/website.sock; +} + +server { + listen 80; + server_name random23900.example.org www.random23900.example.org; + + if ($host != 'random23900.example.org') { + rewrite ^/(.*)$ http://random23900.example.org/$1 permanent; + } + + location ^~ /media/ { + alias /srv/http/random29467/acceptance/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location ^~ /static/ { + alias /srv/http/random29467/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random23900.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + satisfy any; + allow 89.188.25.162; + auth_basic "random29467 acceptance"; + auth_basic_user_file htpasswords/random29467_acceptance; + + } + + access_log /var/log/nginx/random29467/acceptance/access.log combined_plus; + error_log /var/log/nginx/random29467/acceptance/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11849 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11849 new file mode 100644 index 000000000..8a8c90b7e --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11849 @@ -0,0 +1,36 @@ +upstream django_server_random3140.example.org { + server unix:/srv/http/random2912/live/website.sock; +} + +server { + listen 80; + server_name random3140.example.org; + + location ^~ /media/ { + alias /srv/http/random2912/live/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random2912/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random3140.example.org; + include /etc/nginx/proxy_params; + + # You can configure access rules here + } + + access_log /var/log/nginx/random2912/live/access.log combined_plus; + error_log /var/log/nginx/random2912/live/error.log; +} + +server { + server_name www.random3140.example.org; + server_name random28398.example.org; + server_name random23689.example.org www.random23689.example.org; + server_name random25863.example.org www.random25863.example.org; + + rewrite ^ http://random3140.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12027 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12027 new file mode 100644 index 000000000..9d74e2098 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12027 @@ -0,0 +1,29 @@ +upstream django_server_random6410.example.org { + server unix:/srv/http/random28641/live/website.sock; +} + +server { + listen 80; + server_name www.random6410.example.org; + + location ~ /static/(.*)$ { + alias /srv/http/random28641/live/website/static/$1; + expires 7d; + } + + location / { + proxy_pass http://django_server_random6410.example.org; + include /etc/nginx/proxy_params; + + proxy_connect_timeout 240; + proxy_read_timeout 240; + } + + access_log /var/log/nginx/random28641/live/access.log combined_plus; + error_log /var/log/nginx/random28641/live/error.log; +} + +server { + server_name random6410.example.org; + rewrite ^ http://www.random6410.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12235 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12235 new file mode 100644 index 000000000..17ba72db4 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12235 @@ -0,0 +1,33 @@ +server { + server_name random18267.example.org; + gzip on; + gzip_min_length 2000; + gzip_proxied any; + gzip_types application/json; + + client_max_body_size 30M; + + root /srv/http/random23264/data; + + # Security + satisfy any; + include /etc/nginx/allow_ytec_ips_params; + deny all; + + # try serving docs and (md5/immutable) directly + location ~ \+(f|doc)/ { + try_files $uri @proxy_to_app; + } + location / { + # XXX how to tell nginx to just refer to @proxy_to_app here? + try_files /.lqkwje @proxy_to_app; + } + location @proxy_to_app { + proxy_pass http://random20604.example.org:4040; + proxy_set_header X-outside-url $scheme://$host; + proxy_set_header X-Real-IP $remote_addr; + } + + access_log /var/log/nginx/random23264/access.log combined_plus; + error_log /var/log/nginx/random23264/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12649 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12649 new file mode 100644 index 000000000..af5a22620 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12649 @@ -0,0 +1,45 @@ +upstream django_server_random10305.example.org { + server unix:/srv/http/random23322/live/website.sock; +} + +server { + listen 80; + server_name random10305.example.org; + + location /media/ { + alias /srv/http/random23322/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random23322/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random10305.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random23322/live/access.log combined_plus; + error_log /var/log/nginx/random23322/live/error.log; +} + +server { + listen 80; + + server_name random13399.example.org; + server_name www.random10305.example.org; + server_name random17958.example.org www.random17958.example.org; + server_name random15266.example.org www.random15266.example.org; + server_name random21296.example.org www.random21296.example.org; + server_name random5261.example.org www.random5261.example.org; + server_name random679.example.org www.random679.example.org; + server_name random31788.example.org www.random31788.example.org; + server_name random22704.example.org www.random22704.example.org; + server_name random17411.example.org www.random17411.example.org; + + return 301 http://random10305.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-13577 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-13577 new file mode 100644 index 000000000..d7a17f76e --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-13577 @@ -0,0 +1,38 @@ +upstream django_server_random30837.example.org { + server unix:/srv/http/random30992/live/website.sock; +} + +server { + listen 80; + server_name www.random30837.example.org; + + location ^~ /media/ { + alias /srv/http/random30992/live/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random30992/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random30837.example.org; + include /etc/nginx/proxy_params; + + # You can configure access rules here + } + + access_log /var/log/nginx/random30992/live/access.log combined_plus; + error_log /var/log/nginx/random30992/live/error.log; +} + +server { + server_name random30837.example.org; + server_name random3263.example.org www.random3263.example.org; + server_name random6771.example.org www.random6771.example.org; + server_name random17696.example.org www.random17696.example.org; + server_name random7179.example.org www.random7179.example.org; + server_name random8127.example.org www.random8127.example.org; + + rewrite ^ http://www.random30837.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-14402 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-14402 new file mode 100644 index 000000000..ca9ca2f61 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-14402 @@ -0,0 +1,33 @@ +upstream django_server_random17705.example.org { + server unix:/srv/http/random8289/internal/website.sock; +} + +server { + listen 80; + server_name random17705.example.org; + + location /media/ { + alias /srv/http/random8289/internal/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random8289/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random17705.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random8289/internal/access.log combined_plus; + error_log /var/log/nginx/random8289/internal/error.log; +} + +server { + server_name www.random17705.example.org; + return 301 http://random17705.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-14430 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-14430 new file mode 100644 index 000000000..7caf7b2a4 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-14430 @@ -0,0 +1,54 @@ +upstream django_server_random17507.example.org { + server unix:/srv/http/random7740/live/website.sock; +} + +server { + listen 80; + server_name random17507.example.org; + + location ^~ /media/ { + alias /srv/http/random7740/live/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random7740/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random17507.example.org; + include /etc/nginx/proxy_params; + + # You can configure access rules here + } + + access_log /var/log/nginx/random7740/live/access.log combined_plus; + error_log /var/log/nginx/random7740/live/error.log; +} + +server { + server_name www.random17507.example.org; + server_name random31197.example.org www.random31197.example.org; + server_name random19579.example.org www.random19579.example.org; + server_name random16629.example.org www.random16629.example.org; + server_name random28363.example.org www.random28363.example.org; + server_name random30185.example.org www.random30185.example.org; + server_name random22326.example.org www.random22326.example.org; + server_name random3622.example.org www.random3622.example.org; + server_name random1463.example.org www.random1463.example.org; + server_name random23341.example.org www.random23341.example.org; + server_name random2214.example.org www.random2214.example.org; + server_name random22684.example.org www.random22684.example.org; + server_name random6606.example.org www.random6606.example.org; + server_name random29138.example.org www.random29138.example.org; + server_name random15109.example.org www.random15109.example.org; + server_name random8002.example.org www.random8002.example.org; + server_name random16836.example.org www.random16836.example.org; + server_name random22283.example.org www.random22283.example.org; + + location = /googleXXXXXXXXXXXXXXXX.html { + alias /srv/http/random7740/live/website/templates/googleXXXXXXXXXXXXXXXX.html; + } + + rewrite ^ http://random17507.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15141 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15141 new file mode 100644 index 000000000..2b2689f09 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15141 @@ -0,0 +1,36 @@ +upstream django_server_acceptatie.random20374.nl { + server unix:/srv/http/random20374/acceptance/website.sock; +} + +server { + listen 80; + server_name random28586.example.org; + + location ^~ /media/ { + alias /srv/http/random20374/acceptance/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random20374/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_acceptatie.random20374.nl; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'acceptance for random20374'; + auth_basic_user_file /srv/http/random20374/acceptance/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random20374/acceptance/access.log combined_plus; + error_log /var/log/nginx/random20374/acceptance/error.log; +} + +server { + server_name www.random28586.example.org; + rewrite ^ http://random28586.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15270 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15270 new file mode 100644 index 000000000..b4f4bd61c --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15270 @@ -0,0 +1,38 @@ +upstream django_server_random6822.example.org { + server unix:/srv/http/random7047/live/website.sock; +} + +server { + listen 8443; + server_name random6822.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random6822.example.org.complete-bundle.crt; + ssl_certificate_key /etc/ssl/private/random6822.example.org.key; + + location /media/ { + alias /srv/http/random7047/live/dynamic/public/; + expires 7d; + } + location /static/ { + alias /srv/http/random7047/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random6822.example.org; + include /etc/nginx/proxy_params; + } + + access_log /var/log/nginx/random7047/live/access.log combined_plus; + error_log /var/log/nginx/random7047/live/error.log; +} + +server { + listen 80; + server_name random6822.example.org; + + rewrite ^/(.*) https://random6822.example.org:8443/$1; +} + + diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15291 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15291 new file mode 100644 index 000000000..fa09bed93 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15291 @@ -0,0 +1,112 @@ +# You may add here your +# server { +# ... +# } +# statements for each of your virtual hosts to this file + +## +# You should look at the following URL's in order to grasp a solid understanding +# of Nginx configuration files in order to fully unleash the power of Nginx. +# http://wiki.nginx.org/Pitfalls +# http://wiki.nginx.org/QuickStart +# http://wiki.nginx.org/Configuration +# +# Generally, you will want to move this file somewhere, and start with a clean +# file but keep this around for reference. Or just disable in sites-enabled. +# +# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. +## + +server { + listen 80 default_server; + listen [::]:80 default_server ipv6only=on; + + root /usr/share/nginx/html; + index index.html index.htm; + + # Make site accessible from http://random20604.example.org/ + server_name random20604.example.org; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to displaying a 404. + try_files $uri $uri/ =404; + # Uncomment to enable naxsi on this location + # include /etc/nginx/naxsi.rules + } + + # Only for nginx-naxsi used with nginx-naxsi-ui : process denied requests + #location /RequestDenied { + # proxy_pass http://127.0.0.1:8080; + #} + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + #error_page 500 502 503 504 /50x.html; + #location = /50x.html { + # root /usr/share/nginx/html; + #} + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # fastcgi_split_path_info ^(.+\.php)(/.+)$; + # # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini + # + # # With php5-cgi alone: + # fastcgi_pass 127.0.0.1:9000; + # # With php5-fpm: + # fastcgi_pass unix:/var/run/php5-fpm.sock; + # fastcgi_index index.php; + # include fastcgi_params; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} +} + + +# another virtual host using mix of IP-, name-, and port-based configuration +# +#server { +# listen 8000; +# listen random20605.example.org:8080; +# server_name random20605.example.org alias another.alias; +# root html; +# index index.html index.htm; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} + + +# HTTPS server +# +#server { +# listen 443; +# server_name random20604.example.org; +# +# root html; +# index index.html index.htm; +# +# ssl on; +# ssl_certificate cert.pem; +# ssl_certificate_key cert.key; +# +# ssl_session_timeout 5m; +# +# ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; +# ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; +# ssl_prefer_server_ciphers on; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15456 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15456 new file mode 100644 index 000000000..273694b51 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15456 @@ -0,0 +1,39 @@ +upstream django_server_random29275.example.org { + server unix:/srv/http/random14353/internal/website.sock; +} + +server { + listen 80; + server_name random29275.example.org; + + location /media/ { + alias /srv/http/random14353/internal/dynamic/public/; + expires 7d; + } + location /static/ { + alias /srv/http/random14353/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random29275.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + + satisfy any; + auth_basic 'internal for random14353'; + auth_basic_user_file /srv/http/random14353/internal/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random14353/internal/access.log; + error_log /var/log/nginx/random14353/internal/error.log; +} + +server { + server_name www.random29275.example.org; + return 301 http://random29275.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15497 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15497 new file mode 100644 index 000000000..86a8980d2 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15497 @@ -0,0 +1,35 @@ +upstream django_server_random16112.example.org { + server unix:/srv/http/random29227/live/website.sock; +} + +server { + listen 80; + server_name random16112.example.org; + + location /media/ { + alias /srv/http/random29227/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random29227/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random16112.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random29227/live/access.log combined_plus; + error_log /var/log/nginx/random29227/live/error.log; +} +server { + server_name random5297.example.org www.random5297.example.org; + server_name random17050.example.org www.random17050.example.org; + server_name www.random16112.example.org; + + return 301 http://random16112.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15852 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15852 new file mode 100644 index 000000000..32b88c62f --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15852 @@ -0,0 +1,38 @@ +upstream django_server_random7474.example.org { + server unix:/srv/http/random4886/acceptance/website.sock; +} + +server { + listen 80; + server_name random7474.example.org; + + location /media/ { + alias /srv/http/random4886/acceptance/dynamic/public/; + expires 7d; + } + location /static/ { + alias /srv/http/random4886/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random7474.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'acceptance for random4886'; + auth_basic_user_file /srv/http/random4886/acceptance/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + client_max_body_size 20m; + + access_log /var/log/nginx/random4886/acceptance/access.log; + error_log /var/log/nginx/random4886/acceptance/error.log; +} + +server { + server_name www.random7474.example.org; + return 301 http://random7474.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-16345 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-16345 new file mode 100644 index 000000000..ac8ce609c --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-16345 @@ -0,0 +1,34 @@ +upstream django_server_random25713.example.org { + server unix:/srv/http/random24922/live/website.sock; +} + +server { + listen 80; + server_name random25713.example.org; + + location /media/ { + alias /srv/http/random24922/live/dynamic/public/; + expires 7d; + } + location /static/ { + alias /srv/http/random24922/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random25713.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random24922/live/access.log; + error_log /var/log/nginx/random24922/live/error.log; +} + +server { + server_name www.random25713.example.org; + return 301 http://random25713.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17175 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17175 new file mode 100644 index 000000000..e733a70ed --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17175 @@ -0,0 +1,14 @@ +server { + listen 80; + server_name random25647.example.org www.random25647.example.org random10963.example.org www.random10963.example.org; + + if ($host != 'random25647.example.org') { + rewrite ^/(.*)$ http://random25647.example.org/$1 permanent; + } + + index index.html index.htm; + root /srv/http/random11461/countdown/; + + access_log /var/log/nginx/random11461/live/access.log combined_plus; + error_log /var/log/nginx/random11461/live/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17832 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17832 new file mode 100644 index 000000000..4a0967de8 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17832 @@ -0,0 +1,32 @@ +upstream django_server_random6430.example.org { + server unix:/srv/http/random550/internal/website.sock; +} + +server { + listen 80; + server_name random6430.example.org; + + location /media/ { + alias /srv/http/random550/internal/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random550/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random6430.example.org; + include /etc/nginx/django_proxy_params; + + } + + access_log /var/log/nginx/random550/internal/access.log combined_plus; + error_log /var/log/nginx/random550/internal/error.log; +} + +server { + server_name www.random6430.example.org; + return 301 http://random6430.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17942 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17942 new file mode 100644 index 000000000..a3b10eed6 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17942 @@ -0,0 +1,32 @@ +upstream django_server_random25647.example.org { + server unix:/srv/http/random11461/live/website.sock; +} + +server { + listen 80; + server_name random25647.example.org www.random25647.example.org random10963.example.org www.random10963.example.org; + + if ($host != 'random25647.example.org') { + rewrite ^/(.*)$ http://random25647.example.org/$1 permanent; + } + + location /media/ { + alias /srv/http/random11461/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random11461/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random25647.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random11461/live/access.log combined_plus; + error_log /var/log/nginx/random11461/live/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-18018 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-18018 new file mode 100644 index 000000000..63b68d6ff --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-18018 @@ -0,0 +1,36 @@ +upstream django_server_intern.random20374.nl { + server unix:/srv/http/random20374/internal/website.sock; +} + +server { + listen 80; + server_name random23818.example.org; + + location ^~ /media/ { + alias /srv/http/random20374/internal/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random20374/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_intern.random20374.nl; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'internal for random20374'; + auth_basic_user_file /srv/http/random20374/internal/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random20374/internal/access.log combined_plus; + error_log /var/log/nginx/random20374/internal/error.log; +} + +server { + server_name www.random23818.example.org; + rewrite ^ http://random23818.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-18069 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-18069 new file mode 100644 index 000000000..d6d4e5bea --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-18069 @@ -0,0 +1,39 @@ +upstream django_server_random7949.example.org { + server unix:/srv/http/random1006/acceptance/website.sock; +} + +server { + listen 80; + server_name random7949.example.org; + gzip on; + gzip_http_version 1.0; + gzip_types *; + gzip_vary on; + gzip_proxied any; + + location ~ /media/(.*)$ { + alias /srv/http/random1006/acceptance/website/static/$1; + expires 7d; + gzip on; + } + + + location / { + proxy_pass http://django_server_random7949.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'acceptance for random1006'; + auth_basic_user_file /srv/http/random1006/acceptance/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random1006/acceptance/access.log combined_plus; + error_log /var/log/nginx/random1006/acceptance/error.log; +} + +server { + server_name www.random7949.example.org; + rewrite ^ http://random7949.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19334 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19334 new file mode 100644 index 000000000..2609e2080 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19334 @@ -0,0 +1,39 @@ +upstream django_server_random1515.example.org { + server unix:/srv/http/random15255/acceptance/website.sock fail_timeout=5; +} + +server { + listen 80; + server_name random1515.example.org www.random1515.example.org; + + if ($host != 'random1515.example.org') { + rewrite ^/(.*)$ http://random1515.example.org/$1 permanent; + } + + location /media/ { + alias /srv/http/random15255/acceptance/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random15255/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random1515.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + + satisfy any; + auth_basic 'random191 acceptance'; + auth_basic_user_file /srv/http/random15255/acceptance/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random15255/acceptance/access.log combined_plus; + error_log /var/log/nginx/random15255/acceptance/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19639 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19639 new file mode 100644 index 000000000..617472e0d --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19639 @@ -0,0 +1,39 @@ +upstream django_server_live.random8289.random17507.example.org { + server unix:/srv/http/random8289/live/website.sock; +} + +server { + listen 443; + server_name random23886.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random23886.example.org.complete-bundle.crt; + ssl_certificate_key /etc/ssl/private/random23886.example.org.key; + + location /media/ { + alias /srv/http/random8289/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random8289/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_live.random8289.random17507.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + } + + access_log /var/log/nginx/random8289/live/access.log combined_plus; + error_log /var/log/nginx/random8289/live/error.log; +} + +server { + listen 80; + server_name random23886.example.org; + return 301 https://random23886.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-1966 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-1966 new file mode 100644 index 000000000..41aaef04d --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-1966 @@ -0,0 +1,36 @@ +upstream django_server_random31523.example.org { + server unix:/srv/http/random16722.example.org/internal/website.sock; +} + +server { + listen 80; + server_name random31523.example.org; + + location ^~ /media/ { + alias /srv/http/random16722.example.org/internal/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random16722.example.org/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random31523.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'internal for random16722.example.org'; + auth_basic_user_file /srv/http/random16722.example.org/internal/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random16722.example.org/internal/access.log combined_plus; + error_log /var/log/nginx/random16722.example.org/internal/error.log; +} + +server { + server_name www.random31523.example.org; + rewrite ^ http://random31523.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19791 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19791 new file mode 100644 index 000000000..6e3112ad8 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19791 @@ -0,0 +1,34 @@ +upstream django_server_random1413.example.org { + server unix:/srv/http/random25151/live/website.sock; +} + +server { + listen 80; + server_name random1413.example.org; + + location ^~ /media/ { + alias /srv/http/random25151/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location ^~ /static/ { + alias /srv/http/random25151/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random1413.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random25151/live/access.log combined_plus; + error_log /var/log/nginx/random25151/live/error.log; +} + +server { + server_name www.random1413.example.org; + server_name random28524.example.org www.random28524.example.org; + rewrite ^ http://random1413.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19955 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19955 new file mode 100644 index 000000000..20d718409 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19955 @@ -0,0 +1,36 @@ +upstream django_server_random9619.example.org { + server unix:/srv/http/random28641/internal/website.sock; +} + +server { + listen 80; + server_name random9619.example.org; + + location ^~ /media/ { + alias /srv/http/random28641/internal/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random28641/internal/website/static/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random9619.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'internal for random28641'; + auth_basic_user_file /srv/http/random28641/internal/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random28641/internal/access.log combined_plus; + error_log /var/log/nginx/random28641/internal/error.log; +} + +server { + server_name www.random9619.example.org; + rewrite ^ http://random9619.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-21369 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-21369 new file mode 100644 index 000000000..5650efb4c --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-21369 @@ -0,0 +1,33 @@ +upstream django_server_random31758.example.org { + server unix:/srv/http/random21623/internal/website.sock; +} + +server { + listen 80; + server_name random31758.example.org www.random31758.example.org; + + if ($host != 'random31758.example.org') { + rewrite ^/(.*)$ http://random31758.example.org/$1 permanent; + } + + location /media/ { + alias /srv/http/random21623/internal/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random21623/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random31758.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + } + + access_log /var/log/nginx/random21623/internal/access.log combined_plus; + error_log /var/log/nginx/random21623/internal/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-21549 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-21549 new file mode 100644 index 000000000..85576da76 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-21549 @@ -0,0 +1,32 @@ +upstream django_server_random1688.example.org { + server unix:/srv/http/random6470/acceptance/website.sock; +} + +server { + listen 80; + server_name random5078.example.org random1688.example.org www.random1688.example.org; + + if ($host != 'random5078.example.org') { + rewrite ^/(.*)$ http://random5078.example.org/$1 permanent; + } + + location ^~ /media/ { + alias /srv/http/random6470/acceptance/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location ^~ /static/ { + alias /srv/http/random6470/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random1688.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random6470/acceptance/access.log combined_plus; + error_log /var/log/nginx/random6470/acceptance/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-230 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-230 new file mode 100644 index 000000000..00d1d2b0b --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-230 @@ -0,0 +1,33 @@ +upstream django_server_random22746.example.org { + server unix:/srv/http/random6344/internal/website.sock; +} + +server { + listen 80; + server_name random22746.example.org; + + if ($host != 'random22746.example.org') { + rewrite ^/(.*)$ http://random22746.example.org/$1 permanent; + } + + location /media/ { + alias /srv/http/random6344/internal/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random6344/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random22746.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + } + + access_log /var/log/nginx/random6344/internal/access.log combined_plus; + error_log /var/log/nginx/random6344/internal/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23325 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23325 new file mode 100644 index 000000000..5b91f0eaf --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23325 @@ -0,0 +1,74 @@ +upstream django_server_random15255_live { + server unix:/srv/http/random15255/live/website.sock fail_timeout=5; +} + +server { + listen 443; + server_name random7381.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random7381.example.org_chained.crt; + ssl_certificate_key /etc/ssl/private/random7381.example.org.key; + + location /media/ { + alias /srv/http/random15255/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + + location /static/ { + alias /srv/http/random15255/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random15255_live; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + } + + access_log /var/log/nginx/random15255/live/access.log combined_plus; + error_log /var/log/nginx/random15255/live/error.log; +} + +server { + listen 80; + server_name random7381.example.org www.random7381.example.org; + + return 301 https://random7381.example.org$request_uri; +} + +server { + listen 8445; + server_name random7381.example.org www.random7381.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random7381.example.org_chained.crt; + ssl_certificate_key /etc/ssl/private/random7381.example.org.key; + + return 301 https://random7381.example.org$request_uri; +} + +server { + listen 1000; + server_name random7381.example.org www.random7381.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random7381.example.org_chained.crt; + ssl_certificate_key /etc/ssl/private/random7381.example.org.key; + + return 301 https://random7381.example.org$request_uri; +} + +server { + listen 443; + server_name www.random7381.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random7381.example.org_chained.crt; + ssl_certificate_key /etc/ssl/private/random7381.example.org.key; + + return 301 https://random7381.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23470 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23470 new file mode 100644 index 000000000..4f78b645b --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23470 @@ -0,0 +1,56 @@ +upstream django_server_random27579.example.org { + server unix:/srv/http/random21623/live/website.sock; +} + +server { + listen 443; + server_name random27579.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random27579.example.org.bundle.crt; + ssl_certificate_key /etc/ssl/private/random27579.example.org.key; + + location /media/ { + alias /srv/http/random21623/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random21623/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random27579.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + } + + access_log /var/log/nginx/random21623/live/access.log combined_plus; + error_log /var/log/nginx/random21623/live/error.log; +} + +server { + listen 443; + server_name www.random27579.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random27579.example.org.bundle.crt; + ssl_certificate_key /etc/ssl/private/random27579.example.org.key; + + return 301 https://random27579.example.org$request_uri; +} + +server { + listen 80; + + server_name random27579.example.org www.random27579.example.org random11512.example.org; + server_name random18003.example.org www.random18003.example.org; + server_name random26730.example.org www.random26730.example.org; + server_name random3968.example.org www.random3968.example.org; + server_name random11925.example.org www.random11925.example.org; + + return 301 https://random27579.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23791 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23791 new file mode 100644 index 000000000..25933cebb --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23791 @@ -0,0 +1,33 @@ +upstream django_server_random31057.example.org { + server unix:/srv/http/random22194/acceptance/website.sock; +} + +server { + listen 80; + server_name random31057.example.org www.random31057.example.org; + + if ($host != 'random31057.example.org') { + rewrite ^/(.*)$ http://random31057.example.org/$1 permanent; + } + + location /media/ { + alias /srv/http/random22194/acceptance/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random22194/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random31057.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 120; + } + + access_log /var/log/nginx/random22194/acceptance/access.log combined_plus; + error_log /var/log/nginx/random22194/acceptance/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23803 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23803 new file mode 100644 index 000000000..9db2c07f5 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23803 @@ -0,0 +1,32 @@ +upstream django_server_random16722.example.org { + server unix:/srv/http/random16722.example.org/live/website.sock; +} + +server { + listen 80; + server_name random16722.example.org; + + location ^~ /media/ { + alias /srv/http/random16722.example.org/live/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random16722.example.org/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random16722.example.org; + include /etc/nginx/proxy_params; + + # You can configure access rules here + } + + access_log /var/log/nginx/random16722.example.org/live/access.log combined_plus; + error_log /var/log/nginx/random16722.example.org/live/error.log; +} + +server { + server_name www.random16722.example.org; + rewrite ^ http://random16722.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23838 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23838 new file mode 100644 index 000000000..7bd3f2778 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23838 @@ -0,0 +1,32 @@ +upstream django_server_random14388.example.org { + server unix:/srv/http/random4886/live/website.sock; +} + +server { + listen 80; + server_name random14388.example.org; + + location /media/ { + alias /srv/http/random4886/live/dynamic/public/; + expires 7d; + } + location /static/ { + alias /srv/http/random4886/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random14388.example.org; + include /etc/nginx/proxy_params; + + # You can configure access rules here + } + + access_log /var/log/nginx/random4886/live/access.log; + error_log /var/log/nginx/random4886/live/error.log; +} + +server { + server_name www.random14388.example.org; + return 301 http://random14388.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24125 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24125 new file mode 100644 index 000000000..f7efda324 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24125 @@ -0,0 +1,7 @@ +server { + listen 80; + server_name random14996.example.org; + + root /srv/http/random23392/; + index index.html; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24193 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24193 new file mode 100644 index 000000000..1d2b7ec83 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24193 @@ -0,0 +1,62 @@ +upstream django_server_random6177.example.org { + server unix:/srv/http/random550/live/website.sock; +} + +server { + listen 443 ssl; + server_name random2179.example.org; + + ssl_certificate /etc/ssl/public/random2179.example.org.bundle.crt; + ssl_certificate_key /etc/ssl/private/random2179.example.org.key; + + + location /media/ { + alias /srv/http/random550/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random550/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random6177.example.org; + include /etc/nginx/django_proxy_params; + } + + access_log /var/log/nginx/random550/live/access.log combined_plus; + error_log /var/log/nginx/random550/live/error.log; +} + +server { + listen 80; + server_name random2179.example.org; + + location /media/ { + alias /srv/http/random550/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random550/live/static_collected/; + expires 7d; + } + + #location = / { + # return 301 https://random2179.example.org$request_uri; + #} + + location / { + proxy_pass http://django_server_random6177.example.org; + include /etc/nginx/django_proxy_params; + } + + access_log /var/log/nginx/random550/live/access_http.log combined_plus; + error_log /var/log/nginx/random550/live/error_http.log; +} + +server { + server_name random6177.example.org www.random6177.example.org; + return 301 http://random2179.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24213 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24213 new file mode 100644 index 000000000..b23aeae19 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24213 @@ -0,0 +1,36 @@ +upstream django_server_random22047.example.org { + server unix:/srv/http/random26975/acceptance/website.sock; +} + +server { + listen 80; + server_name random22047.example.org; + + location /media/ { + alias /srv/http/random26975/acceptance/dynamic/public/; + expires 7d; + } + location /static/ { + alias /srv/http/random26975/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random22047.example.org; + include /etc/nginx/django_proxy_params; + + satisfy any; + auth_basic 'acceptance for random26975'; + auth_basic_user_file /srv/http/random26975/acceptance/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random26975/acceptance/access.log; + error_log /var/log/nginx/random26975/acceptance/error.log; +} + +server { + server_name www.random22047.example.org; + return 301 http://random22047.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-25480 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-25480 new file mode 100644 index 000000000..7628d27d2 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-25480 @@ -0,0 +1,32 @@ +upstream django_server_random6193.example.org { + server unix:/srv/http/random4755/live/website.sock; +} + +server { + listen 80; + server_name random6193.example.org www.random6193.example.org; + + if ($host != 'random6193.example.org') { + rewrite ^/(.*)$ http://random6193.example.org/$1 permanent; + } + + location /media/ { + alias /srv/http/random4755/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random4755/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random6193.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random4755/live/access.log combined_plus; + error_log /var/log/nginx/random4755/live/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26195 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26195 new file mode 100644 index 000000000..232935a51 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26195 @@ -0,0 +1,26 @@ +server { + listen 80; + server_name www.random25446.example.org random25446.example.org; + + if ($host != 'random25446.example.org') { + rewrite ^/(.*)$ http://random25446.example.org/$1 permanent; + } + + location ^~ /media { + alias /srv/http/random17476/internal/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location ^~ /static { + alias /srv/http/random17476/internal/static_collected/; + expires 7d; + } + + location / { + include fastcgi_params; + fastcgi_pass unix:/srv/http/random17476/internal/website.sock; + } + + access_log /var/log/nginx/random17476/internal/access.log combined_plus; + error_log /var/log/nginx/random17476/internal/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26221 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26221 new file mode 100644 index 000000000..8e5893d61 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26221 @@ -0,0 +1,32 @@ +upstream django_server_random4030.example.org { + server unix:/srv/http/random26975/live/website.sock; +} + +server { + listen 80; + server_name random4030.example.org; + + location /media/ { + alias /srv/http/random26975/live/dynamic/public/; + expires 7d; + } + location /static/ { + alias /srv/http/random26975/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random4030.example.org; + include /etc/nginx/django_proxy_params; + + # You can configure access rules here + } + + access_log /var/log/nginx/random26975/live/access.log; + error_log /var/log/nginx/random26975/live/error.log; +} + +server { + server_name www.random4030.example.org; + return 301 http://random4030.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26637 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26637 new file mode 100644 index 000000000..3ef549982 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26637 @@ -0,0 +1,32 @@ +upstream django_server_random5890.example.org { + server unix:/srv/http/random4755/internal/website.sock; +} + +server { + listen 80; + server_name random5890.example.org; + + if ($host != 'random5890.example.org') { + rewrite ^/(.*)$ http://random5890.example.org/$1 permanent; + } + + location /media/ { + alias /srv/http/random4755/internal/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random4755/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random5890.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random4755/internal/access.log combined_plus; + error_log /var/log/nginx/random4755/internal/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26758 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26758 new file mode 100644 index 000000000..f7cfb854c --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26758 @@ -0,0 +1,21 @@ +server { + listen 80 default_server; + #listen [::]:80 default_server ipv6only=on; + root /var/www/default/; + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + location ~ /\.ht { + deny all; + } + + location /nginx_status { + stub_status on; + access_log off; + allow 127.0.0.1; + deny all; + } + + access_log /var/log/nginx/access.log combined_plus; + error_log /var/log/nginx/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27646 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27646 new file mode 100644 index 000000000..9328e2943 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27646 @@ -0,0 +1,37 @@ +upstream django_server_random10783.example.org { + server unix:/srv/http/random4711/acceptance/website.sock; +} + +server { + listen 80; + server_name random10783.example.org; + + location ^~ /media/ { + alias /srv/http/random4711/acceptance/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random4711/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random10783.example.org; + include /etc/nginx/proxy_params; + proxy_read_timeout 4m; + + satisfy any; + auth_basic 'acceptance for random4711'; + auth_basic_user_file /srv/http/random4711/acceptance/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random4711/acceptance/access.log combined_plus; + error_log /var/log/nginx/random4711/acceptance/error.log; +} + +server { + server_name www.random10783.example.org; + rewrite ^ http://random10783.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27728 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27728 new file mode 100644 index 000000000..fdef2900c --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27728 @@ -0,0 +1,5 @@ +server { + location =/ { + return 404; + } +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27736 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27736 new file mode 100644 index 000000000..5f579971a --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27736 @@ -0,0 +1,32 @@ +upstream django_server_random17112.example.org { + server unix:/srv/http/random29467/live/website.sock; +} + +server { + listen 80; + server_name random17112.example.org www.random17112.example.org; + + if ($host != 'random17112.example.org') { + rewrite ^/(.*)$ http://random17112.example.org/$1 permanent; + } + + location ^~ /media/ { + alias /srv/http/random29467/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location ^~ /static/ { + alias /srv/http/random29467/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random17112.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random29467/live/access.log combined_plus; + error_log /var/log/nginx/random29467/live/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27812 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27812 new file mode 100644 index 000000000..8e455eb9b --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27812 @@ -0,0 +1,36 @@ +upstream django_server_random1296.example.org { + server unix:/srv/http/random2912/acceptance/website.sock; +} + +server { + listen 80; + server_name random1296.example.org; + + location ^~ /media/ { + alias /srv/http/random2912/acceptance/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random2912/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random1296.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'acceptance for random2912'; + auth_basic_user_file /srv/http/random2912/acceptance/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random2912/acceptance/access.log combined_plus; + error_log /var/log/nginx/random2912/acceptance/error.log; +} + +server { + server_name www.random1296.example.org; + rewrite ^ http://random1296.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-28050 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-28050 new file mode 100644 index 000000000..3d0ac97ae --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-28050 @@ -0,0 +1,36 @@ +upstream django_server_random11685.example.org { + server unix:/srv/http/random4886/internal/website.sock; +} + +server { + listen 80; + server_name random11685.example.org; + + location /media/ { + alias /srv/http/random4886/internal/dynamic/public/; + expires 7d; + } + location /static/ { + alias /srv/http/random4886/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random11685.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'internal for random4886'; + auth_basic_user_file /srv/http/random4886/internal/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random4886/internal/access.log; + error_log /var/log/nginx/random4886/internal/error.log; +} + +server { + server_name www.random11685.example.org; + return 301 http://random11685.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-28690 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-28690 new file mode 100644 index 000000000..69bcb26c0 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-28690 @@ -0,0 +1,32 @@ +upstream django_server_random16112.example.org { + server unix:/srv/http/random24645/live/website.sock; +} + +server { + listen 80; + server_name random16112.example.org; + + location ^~ /media/ { + alias /srv/http/random24645/live/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random24645/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random16112.example.org; + include /etc/nginx/proxy_params; + + # You can configure access rules here + } + + access_log /var/log/nginx/random24645/live/access.log; + error_log /var/log/nginx/random24645/live/error.log; +} + +server { + server_name www.random16112.example.org; + rewrite ^ http://random16112.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-29159 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-29159 new file mode 100644 index 000000000..be6481eae --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-29159 @@ -0,0 +1,33 @@ +upstream django_server_random29198.example.org { + server unix:/srv/http/random28641/acceptance/website.sock; +} + +server { + listen 80; + server_name random29198.example.org; + + location ~ /static/(.*)$ { + alias /srv/http/random28641/acceptance/website/static/$1; + expires 7d; + } + + + location / { + proxy_pass http://django_server_random29198.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'acceptance for random28641'; + auth_basic_user_file /srv/http/random28641/acceptance/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random28641/acceptance/access.log combined_plus; + error_log /var/log/nginx/random28641/acceptance/error.log; +} + +server { + server_name www.random29198.example.org; + rewrite ^ http://random29198.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-2951 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-2951 new file mode 100644 index 000000000..683aa3226 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-2951 @@ -0,0 +1,67 @@ +server { + listen 80; + #listen [::]:80 default_server ipv6only=on; + root /var/www/random616_log/; + server_name random12800.example.org; + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + location ~ \.php$ { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini + + # With php5-fpm: + fastcgi_pass unix:/var/run/php5-fpm.sock; + fastcgi_index index.php; + include fastcgi_params; + } + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + location ~ /\.ht { + deny all; + } + + location /nginx_status { + stub_status on; + access_log off; + allow 127.0.0.1; + deny all; + } + + access_log /var/log/nginx/random12543/access.log combined_plus; + error_log /var/log/nginx/random12543/error.log; +} + +server { + listen 443 default_server; + #listen [::]:443 default_server ipv6only=on; + root /var/www/random616_log/; + server_name random12800.example.org; + + # We created (will create) this SSL certificate ourselves, using our own CA. This way, we can control strictly which CA the XXX trusts. + # See ytec #6244 + # However, we're working on a fix for high SSL overhead. We're hoping to be able to keep the connections open between log POSTs, like SSL can. + ssl on; + ssl_certificate /etc/ssl/public/random12800.example.org.crt; + ssl_certificate_key /etc/ssl/private/random12800.example.org.key; + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + location ~ \.php$ { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini + + # With php5-fpm: + fastcgi_pass unix:/var/run/php5-fpm.sock; + fastcgi_index index.php; + include fastcgi_params; + } + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + location ~ /\.ht { + deny all; + } + + access_log /var/log/nginx/random12543/access.log combined_plus; + error_log /var/log/nginx/random12543/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-30011 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-30011 new file mode 100644 index 000000000..479edac5d --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-30011 @@ -0,0 +1,37 @@ +upstream django_server_random12785.example.org { + server unix:/srv/http/random14353/live/website.sock; +} + +server { + listen 80; + server_name random12785.example.org; + + location /media/ { + alias /srv/http/random14353/live/dynamic/public/; + expires 7d; + } + location /static/ { + alias /srv/http/random14353/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random12785.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + + satisfy any; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random14353/live/access.log; + error_log /var/log/nginx/random14353/live/error.log; +} + +server { + server_name www.random12785.example.org; + return 301 http://random12785.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-30571 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-30571 new file mode 100644 index 000000000..84e44dd7c --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-30571 @@ -0,0 +1,31 @@ +upstream django_server_random7150.example.org { + server unix:/srv/http/random550/acceptance/website.sock; +} + +server { + listen 80; + server_name random7150.example.org; + + location /media/ { + alias /srv/http/random550/acceptance/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random550/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random7150.example.org; + include /etc/nginx/django_proxy_params; + } + + access_log /var/log/nginx/random550/acceptance/access.log combined_plus; + error_log /var/log/nginx/random550/acceptance/error.log; +} + +server { + server_name www.random7150.example.org; + return 301 http://random7150.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-31900 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-31900 new file mode 100644 index 000000000..648693cbc --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-31900 @@ -0,0 +1,33 @@ +upstream django_server_random31131.example.org { + server unix:/srv/http/random24334/internal/website.sock; +} + +server { + listen 80; + server_name random31131.example.org; + + location /media/ { + alias /srv/http/random24334/internal/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random24334/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random31131.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random24334/internal/access.log combined_plus; + error_log /var/log/nginx/random24334/internal/error.log; +} + +server { + server_name www.random31131.example.org; + return 301 http://random31131.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32190 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32190 new file mode 100644 index 000000000..8c7738c03 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32190 @@ -0,0 +1,4 @@ +server { + server_name www.random5115; + return 301 http://www.random10305.example.org; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32279 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32279 new file mode 100644 index 000000000..16f4e5e9e --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32279 @@ -0,0 +1,25 @@ +server { + listen 80; + root /home/admin/random19651_log/; + server_name random16339.example.org; + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + location ~ \.php$ { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini + + # With php5-fpm: + fastcgi_pass unix:/var/run/php5-fpm.sock; + fastcgi_index index.php; + include fastcgi_params; + } + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + location ~ /\.ht { + deny all; + } + + access_log /var/log/nginx/random4235/access.log combined_plus; + error_log /var/log/nginx/random4235/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32317 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32317 new file mode 100644 index 000000000..e9c986ff1 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32317 @@ -0,0 +1,32 @@ +upstream django_server_random21989.example.org { + server unix:/srv/http/random28136/acceptance/website.sock; +} + +server { + listen 80; + server_name random21989.example.org; + + location ~ /static/(.*)$ { + alias /srv/http/random28136/acceptance/website/static/$1; + expires 7d; + } + + location / { + proxy_pass http://django_server_random21989.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'acceptance for random28136'; + auth_basic_user_file /srv/http/random28136/acceptance/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random28136/acceptance/access.log combined_plus; + error_log /var/log/nginx/random28136/acceptance/error.log; +} + +server { + server_name www.random21989.example.org; + rewrite ^ http://random21989.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32438 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32438 new file mode 100644 index 000000000..66929620f --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32438 @@ -0,0 +1,46 @@ +upstream django_server_random1769.example.org { + server unix:/srv/http/random7047/acceptance/website.sock; +} + +server { + listen 80; + server_name random1769.example.org; + + if ($host != 'random1769.example.org') { + rewrite ^/(.*)$ http://random1769.example.org/$1 permanent; + } + + rewrite ^/(.*) https://$host:8444/$1; +} + +server { + listen 8444; + server_name random1769.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random6822.example.org.crt; + ssl_certificate_key /etc/ssl/private/random6822.example.org.key; + + location ^~ /media/ { + alias /srv/http/random7047/acceptance/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random7047/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random1769.example.org; + include /etc/nginx/proxy_params; + + #satisfy any; + #auth_basic 'acceptance for random7047'; + #auth_basic_user_file /srv/http/random7047/acceptance/htpasswords; + #include /etc/nginx/allow_ytec_ips_params; + #deny all; + } + + access_log /var/log/nginx/random7047/acceptance/access.log combined_plus; + error_log /var/log/nginx/random7047/acceptance/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3483 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3483 new file mode 100644 index 000000000..7a415c293 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3483 @@ -0,0 +1,32 @@ +server { + listen 80; + server_name random9761.example.org; + + + location ~ /static/(.*)$ { + alias /srv/http/random14537/static_collected/$1; + expires 7d; + } + + location ~ /media/(.*)$ { + alias /srv/http/random14537/dynamic/public/$1; + expires 7d; + include upload_folder_security_params; + } + + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $host; + proxy_pass http://127.0.0.1:81; + proxy_connect_timeout 120; + proxy_read_timeout 120; + } + + location ~ /\.ht { + deny all; + } + + access_log /var/log/nginx/random14537/access.log combined_plus; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3507 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3507 new file mode 100644 index 000000000..0fdca78d7 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3507 @@ -0,0 +1,44 @@ +server { + listen 80; + server_name random3674.example.org www.random3674.example.org; + + root /srv/http/random3674.example.org; + index index.html index.htm; + + location / { + try_files $uri $uri/ =404; + } + + access_log /var/log/nginx/random3674.example.org/access.log combined_plus; + error_log /var/log/nginx/random3674.example.org/error.log; +} + +server { + listen 80; + server_name random27569.example.org www.random27569.example.org; + + root /srv/http/random27569.example.org; + index index.html index.htm; + + location / { + try_files $uri $uri/ =404; + } + + access_log /var/log/nginx/random27569.example.org/access.log combined_plus; + error_log /var/log/nginx/random27569.example.org/error.log; +} + +server { + listen 80; + server_name random11055.example.org www.random11055.example.org; + + root /srv/http/random11055.example.org; + index index.html index.htm; + + location / { + try_files $uri $uri/ =404; + } + + access_log /var/log/nginx/random11055.example.org/access.log combined_plus; + error_log /var/log/nginx/random11055.example.org/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3874 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3874 new file mode 100644 index 000000000..1180f2eb1 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3874 @@ -0,0 +1,46 @@ +upstream django_server_random7267.example.org { + server unix:/srv/http/random24334/live/website.sock; +} + +server { + listen 80; + listen 443 ssl; + + server_name random7267.example.org; + + ssl_certificate /etc/ssl/public/random7267.example.org_chained.crt; + ssl_certificate_key /etc/ssl/private/random7267.example.org.key; + + location /media/ { + alias /srv/http/random24334/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random24334/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random7267.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + } + + access_log /var/log/nginx/random24334/live/access.log combined_plus; + error_log /var/log/nginx/random24334/live/error.log; +} + +server { + listen 80; + listen 443 ssl; + + server_name www.random7267.example.org; + + ssl_certificate /etc/ssl/public/random7267.example.org_chained.crt; + ssl_certificate_key /etc/ssl/private/random7267.example.org.key; + + return 301 http://random7267.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4035 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4035 new file mode 100644 index 000000000..1a1deb96b --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4035 @@ -0,0 +1,31 @@ +upstream django_server_random2104.example.org { + server unix:/srv/http/random28136/live/website.sock; +} + +server { + listen 80; + server_name www.random2104.example.org; + + location ~ /static/(.*)$ { + alias /srv/http/random28136/live/website/static/$1; + expires 7d; + } + + + location / { + proxy_pass http://django_server_random2104.example.org; + include /etc/nginx/proxy_params; + proxy_connect_timeout 240; + proxy_read_timeout 240; + + # You can configure access rules here + } + + access_log /var/log/nginx/random28136/live/access.log combined_plus; + error_log /var/log/nginx/random28136/live/error.log; +} + +server { + server_name random2104.example.org; + rewrite ^ http://www.random2104.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4143 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4143 new file mode 100644 index 000000000..add683007 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4143 @@ -0,0 +1,33 @@ +upstream django_server_random24919.example.org { + server unix:/srv/http/random7831/live/website.sock; +} + +server { + listen 80; + server_name random24919.example.org; + + location ^~ /media/ { + alias /srv/http/random7831/live/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random7831/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random24919.example.org; + include /etc/nginx/proxy_params; + + proxy_connect_timeout 240; + proxy_read_timeout 240; + } + + access_log /var/log/nginx/random7831/live/access.log combined_plus; + error_log /var/log/nginx/random7831/live/error.log; +} + +server { + server_name www.random24919.example.org; + rewrite ^ http://random24919.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4264 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4264 new file mode 100644 index 000000000..ef347862f --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4264 @@ -0,0 +1,12 @@ +# vhost created by moving from marauder, but there it was an apache vhost. + +server { + listen 80; + server_name random3080.example.org www.random3080.example.org random26833.example.org www.random26833.example.org; + + root /srv/http/random10391.example.org/; + + if ($request_uri != '/googleYYYYYYYYYYYYYYYY.html') { + rewrite ^ http://random10305.example.org/ permanent; + } +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-5826 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-5826 new file mode 100644 index 000000000..bcfc662b2 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-5826 @@ -0,0 +1,38 @@ +upstream django_server_random1107.example.org { + server unix:/srv/http/random4755/acceptance/website.sock; +} + +server { + listen 80; + server_name random1107.example.org www.random1107.example.org; + + if ($host != 'random1107.example.org') { + rewrite ^/(.*)$ http://random1107.example.org/$1 permanent; + } + + location /media/ { + alias /srv/http/random4755/acceptance/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random4755/acceptance/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random1107.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + satisfy any; + allow 89.188.25.162; + auth_basic "random4755 acceptance"; + auth_basic_user_file htpasswords/random4755_acceptance; + + } + + access_log /var/log/nginx/random4755/acceptance/access.log combined_plus; + error_log /var/log/nginx/random4755/acceptance/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-5872 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-5872 new file mode 100644 index 000000000..fe41f9872 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-5872 @@ -0,0 +1,36 @@ +upstream django_server_random8404.example.org { + server unix:/srv/http/random1006/internal/website.sock; +} + +server { + listen 80; + server_name random8404.example.org; + + location ^~ /media/ { + alias /srv/http/random1006/internal/website/static/; + expires 7d; + } + #location ^~ /static/ { + # alias /srv/http/random1006/internal/website/static/; + # expires 7d; + #} + + location / { + proxy_pass http://django_server_random8404.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'internal for random1006'; + auth_basic_user_file /srv/http/random1006/internal/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random1006/internal/access.log combined_plus; + error_log /var/log/nginx/random1006/internal/error.log; +} + +server { + server_name www.random8404.example.org; + rewrite ^ http://random8404.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-6228 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-6228 new file mode 100644 index 000000000..d5c157e88 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-6228 @@ -0,0 +1,39 @@ +upstream django_server_random15255_intern { + server unix:/srv/http/random15255/intern/website.sock fail_timeout=5; +} + +server { + listen 80; + server_name random11459.example.org www.random11459.example.org; + + if ($host != 'random11459.example.org') { + rewrite ^/(.*)$ http://random11459.example.org/$1 permanent; + } + + location /media/ { + alias /srv/http/random15255/internal/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random15255/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random15255_intern; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + + satisfy any; + auth_basic 'random191 internal'; + auth_basic_user_file /srv/http/random15255/internal/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random15255/internal/access.log combined_plus; + error_log /var/log/nginx/random15255/internal/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-7895 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-7895 new file mode 100644 index 000000000..4a49ea47e --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-7895 @@ -0,0 +1,32 @@ +upstream django_server_random20084.example.org { + server unix:/srv/http/random1540/live/website.sock; +} + +server { + listen 80; + server_name random3969.example.org www.random20084.example.org random20084.example.org; + + if ($host != 'www.random20084.example.org') { + rewrite ^/(.*)$ http://www.random20084.example.org/$1 permanent; + } + + location /media/ { + alias /srv/http/random1540/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random1540/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random20084.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/random1540/live/access.log combined_plus; + error_log /var/log/nginx/random1540/live/error.log; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8343 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8343 new file mode 100644 index 000000000..9e0d39d47 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8343 @@ -0,0 +1,36 @@ +upstream django_server_random29577.example.org { + server unix:/srv/http/random24645/internal/website.sock; +} + +server { + listen 80; + server_name random29577.example.org; + + location ^~ /media/ { + alias /srv/http/random24645/internal/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random24645/internal/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random29577.example.org; + include /etc/nginx/proxy_params; + + satisfy any; + auth_basic 'internal for random24645'; + auth_basic_user_file /srv/http/random24645/internal/htpasswords; + include /etc/nginx/allow_ytec_ips_params; + deny all; + } + + access_log /var/log/nginx/random24645/internal/access.log; + error_log /var/log/nginx/random24645/internal/error.log; +} + +server { + server_name www.random29577.example.org; + rewrite ^ http://random29577.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8422 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8422 new file mode 100644 index 000000000..c3b979b4e --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8422 @@ -0,0 +1,46 @@ +upstream django_server_random25771.example.org { + server unix:/srv/http/random4711/live/website.sock; +} + +server { + listen 80; + server_name random25771.example.org; + + location ^~ /media/ { + alias /srv/http/random4711/live/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random4711/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random25771.example.org; + include /etc/nginx/proxy_params; + proxy_read_timeout 4m; + + # You can configure access rules here + } + + client_max_body_size 25m; + + access_log /var/log/nginx/random4711/live/access.log combined_plus; + error_log /var/log/nginx/random4711/live/error.log; +} + +server { + server_name www.random25771.example.org; + server_name *.random17707.example.org; + server_name *.random22274.example.org; + server_name *.random26333.example.org; + server_name *.random10742.example.org; + server_name *.random8297.example.org; + server_name *.random18250.example.org; + server_name *.random30184.example.org; + server_name *.random27005.example.org; + server_name *.random12286.example.org; + server_name *.random28076.example.org; + server_name *.random26194.example.org; + rewrite ^ http://random25771.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8637 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8637 new file mode 100644 index 000000000..91e31bbfd --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8637 @@ -0,0 +1,40 @@ +upstream django_server_random27891.example.org { + server unix:/srv/http/random6344/live/website.sock; +} + +server { + listen 443; + server_name random27891.example.org; + + ssl on; + ssl_certificate /etc/ssl/public/random27891.example.org.bundle.crt; + ssl_certificate_key /etc/ssl/private/random27891.example.org.key; + + location /media/ { + alias /srv/http/random6344/live/dynamic/public/; + expires 7d; + include upload_folder_security_params; + } + location /static/ { + alias /srv/http/random6344/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random27891.example.org; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + } + + access_log /var/log/nginx/random6344/live/access.log combined_plus; + error_log /var/log/nginx/random6344/live/error.log; +} + +server { + listen 80; + server_name random27891.example.org; + + return 301 https://random27891.example.org$request_uri; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8662 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8662 new file mode 100644 index 000000000..3fe9c4011 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8662 @@ -0,0 +1,32 @@ +upstream django_server_random27507.example.org { + server unix:/srv/http/random24211/live/website.sock; +} + +server { + listen 80; + server_name random27507.example.org; + + location ^~ /media/ { + alias /srv/http/random24211/live/dynamic/public/; + expires 7d; + } + location ^~ /static/ { + alias /srv/http/random24211/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random27507.example.org; + include /etc/nginx/proxy_params; + + # You can configure access rules here + } + + access_log /var/log/nginx/random24211/live/access.log combined_plus; + error_log /var/log/nginx/random24211/live/error.log; +} + +server { + server_name www.random27507.example.org; + rewrite ^ http://random27507.example.org$request_uri permanent; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-9426 b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-9426 new file mode 100644 index 000000000..90dad9601 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-9426 @@ -0,0 +1,111 @@ +upstream django_server_random20374.nl { + server unix:/srv/http/random20374/live/website.sock; +} + +server { + listen 80; + + # Main domain + server_name random9123.example.org; + + # So called mini-sites, resulting in landing pages for Google. + server_name random16942.example.org; + server_name random23560.example.org; + server_name random17636.example.org; + server_name random13969.example.org; + server_name random4892.example.org; + server_name random24240.example.org; + server_name random25863.example.org; + server_name random26503.example.org; + server_name random5090.example.org; + server_name random1856.example.org; + server_name random2911.example.org; + server_name random16405.example.org; + + location /media/ { + alias /srv/http/random20374/live/dynamic/public/; + expires 7d; + } + location /static/ { + alias /srv/http/random20374/live/static_collected/; + expires 7d; + } + + location / { + proxy_pass http://django_server_random20374.nl; + include /etc/nginx/proxy_params; + } + + access_log /var/log/nginx/random20374/live/access.log combined_plus; + error_log /var/log/nginx/random20374/live/error.log; +} + +server { + server_name www.random9123.example.org; + return 301 $scheme://random9123.example.org$request_uri; +} + +server { + server_name www.random1825.example.org random1825.example.org; + return 301 $scheme://random9123.example.org$request_uri; +} + +server { + server_name www.random16942.example.org; + return 301 $scheme://random16942.example.org; +} + +server { + server_name www.random23560.example.org; + return 301 $scheme://random23560.example.org; +} + +server { + server_name www.random17636.example.org; + return 301 $scheme://random17636.example.org; +} + +server { + server_name www.random13969.example.org; + return 301 $scheme://random13969.example.org; +} + +server { + server_name www.random4892.example.org; + return 301 $scheme://random4892.example.org; +} + +server { + server_name www.random24240.example.org; + return 301 $scheme://random24240.example.org; +} + +server { + server_name www.random25863.example.org; + return 301 $scheme://random25863.example.org; +} + +server { + server_name www.random26503.example.org; + return 301 $scheme://random26503.example.org; +} + +server { + server_name www.random5090.example.org; + return 301 $scheme://random5090.example.org; +} + +server { + server_name www.random1856.example.org; + return 301 $scheme://random1856.example.org; +} + +server { + server_name www.random2911.example.org; + return 301 $scheme://random2911.example.org; +} + +server { + server_name www.random16405.example.org; + return 301 $scheme://random16405.example.org; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/activecolab/www.example.com.vhost b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/activecolab/www.example.com.vhost new file mode 100644 index 000000000..71344abea --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/activecolab/www.example.com.vhost @@ -0,0 +1,44 @@ +server { + listen 80; + server_name www.example.com example.com; + root /var/www/www.example.com/web; + + if ($http_host != "www.example.com") { + rewrite ^ http://www.example.com$request_uri permanent; + } + + index index.php index.html; + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac). + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + location / { + try_files $uri $uri/ /index.php?path_info=$uri&$args; + access_log off; + expires max; + } + + location ~ \.php$ { + try_files $uri =404; + include /etc/nginx/fastcgi_params; + fastcgi_pass unix:/var/run/php5-fpm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_intercept_errors on; + } +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/fastcgi.conf b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/fastcgi.conf new file mode 100644 index 000000000..056987136 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/fastcgi.conf @@ -0,0 +1,9 @@ +#-*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: "american" -*- +### fastcgi configuration. +fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; +include fastcgi_params; +fastcgi_buffers 256 4k; +fastcgi_intercept_errors on; +## allow 4 hrs - pass timeout responsibility to upstrea +fastcgi_read_timeout 14400; +fastcgi_index index.php; diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/fastcgi_params b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/fastcgi_params new file mode 100644 index 000000000..4a7f26920 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/fastcgi_params @@ -0,0 +1,32 @@ +# -*- mode: conf; mode: flyspell-prog; ispell-local-dictionary: "american" -*- +### fastcgi parameters. +fastcgi_param QUERY_STRING $query_string; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; + +fastcgi_param SCRIPT_NAME $fastcgi_script_name; +fastcgi_param REQUEST_URI $request_uri; +fastcgi_param DOCUMENT_URI $document_uri; +fastcgi_param DOCUMENT_ROOT $document_root; +fastcgi_param SERVER_PROTOCOL $server_protocol; + +fastcgi_param GATEWAY_INTERFACE CGI/1.1; +fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + +fastcgi_param REMOTE_ADDR $remote_addr; +fastcgi_param REMOTE_PORT $remote_port; +fastcgi_param SERVER_ADDR $server_addr; +fastcgi_param SERVER_PORT $server_port; +fastcgi_param SERVER_NAME $server_name; +fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + +## PHP only, required if PHP was built with --enable-force-cgi-redirect +fastcgi_param REDIRECT_STATUS 200; +## HTTPS 'on' parameter. This requires Nginx version 1.1.11 or +## later. The if_not_empty flag was introduced in 1.1.11. See: +## http://nginx.org/en/CHANGES. If using a version that doesn't +## support this comment out the line below. +fastcgi_param HTTPS $https if_not_empty; +## For Nginx versions below 1.1.11 uncomment the line below after commenting out the above. +#fastcgi_param HTTPS $https diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/koi-utf b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/koi-utf new file mode 100644 index 000000000..e7974ff6a --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/koi-utf @@ -0,0 +1,109 @@ + +# This map is not a full koi8-r <> utf8 map: it does not contain +# box-drawing and some other characters. Besides this map contains +# several koi8-u and Byelorussian letters which are not in koi8-r. +# If you need a full and standard map, use contrib/unicode2nginx/koi-utf +# map instead. + +charset_map koi8-r utf-8 { + + 80 E282AC ; # euro + + 95 E280A2 ; # bullet + + 9A C2A0 ; #   + + 9E C2B7 ; # · + + A3 D191 ; # small yo + A4 D194 ; # small Ukrainian ye + + A6 D196 ; # small Ukrainian i + A7 D197 ; # small Ukrainian yi + + AD D291 ; # small Ukrainian soft g + AE D19E ; # small Byelorussian short u + + B0 C2B0 ; # ° + + B3 D081 ; # capital YO + B4 D084 ; # capital Ukrainian YE + + B6 D086 ; # capital Ukrainian I + B7 D087 ; # capital Ukrainian YI + + B9 E28496 ; # numero sign + + BD D290 ; # capital Ukrainian soft G + BE D18E ; # capital Byelorussian short U + + BF C2A9 ; # (C) + + C0 D18E ; # small yu + C1 D0B0 ; # small a + C2 D0B1 ; # small b + C3 D186 ; # small ts + C4 D0B4 ; # small d + C5 D0B5 ; # small ye + C6 D184 ; # small f + C7 D0B3 ; # small g + C8 D185 ; # small kh + C9 D0B8 ; # small i + CA D0B9 ; # small j + CB D0BA ; # small k + CC D0BB ; # small l + CD D0BC ; # small m + CE D0BD ; # small n + CF D0BE ; # small o + + D0 D0BF ; # small p + D1 D18F ; # small ya + D2 D180 ; # small r + D3 D181 ; # small s + D4 D182 ; # small t + D5 D183 ; # small u + D6 D0B6 ; # small zh + D7 D0B2 ; # small v + D8 D18C ; # small soft sign + D9 D18B ; # small y + DA D0B7 ; # small z + DB D188 ; # small sh + DC D18D ; # small e + DD D189 ; # small shch + DE D187 ; # small ch + DF D18A ; # small hard sign + + E0 D0AE ; # capital YU + E1 D090 ; # capital A + E2 D091 ; # capital B + E3 D0A6 ; # capital TS + E4 D094 ; # capital D + E5 D095 ; # capital YE + E6 D0A4 ; # capital F + E7 D093 ; # capital G + E8 D0A5 ; # capital KH + E9 D098 ; # capital I + EA D099 ; # capital J + EB D09A ; # capital K + EC D09B ; # capital L + ED D09C ; # capital M + EE D09D ; # capital N + EF D09E ; # capital O + + F0 D09F ; # capital P + F1 D0AF ; # capital YA + F2 D0A0 ; # capital R + F3 D0A1 ; # capital S + F4 D0A2 ; # capital T + F5 D0A3 ; # capital U + F6 D096 ; # capital ZH + F7 D092 ; # capital V + F8 D0AC ; # capital soft sign + F9 D0AB ; # capital Y + FA D097 ; # capital Z + FB D0A8 ; # capital SH + FC D0AD ; # capital E + FD D0A9 ; # capital SHCH + FE D0A7 ; # capital CH + FF D0AA ; # capital hard sign +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/koi-win b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/koi-win new file mode 100644 index 000000000..72afabe89 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/koi-win @@ -0,0 +1,103 @@ + +charset_map koi8-r windows-1251 { + + 80 88 ; # euro + + 95 95 ; # bullet + + 9A A0 ; #   + + 9E B7 ; # · + + A3 B8 ; # small yo + A4 BA ; # small Ukrainian ye + + A6 B3 ; # small Ukrainian i + A7 BF ; # small Ukrainian yi + + AD B4 ; # small Ukrainian soft g + AE A2 ; # small Byelorussian short u + + B0 B0 ; # ° + + B3 A8 ; # capital YO + B4 AA ; # capital Ukrainian YE + + B6 B2 ; # capital Ukrainian I + B7 AF ; # capital Ukrainian YI + + B9 B9 ; # numero sign + + BD A5 ; # capital Ukrainian soft G + BE A1 ; # capital Byelorussian short U + + BF A9 ; # (C) + + C0 FE ; # small yu + C1 E0 ; # small a + C2 E1 ; # small b + C3 F6 ; # small ts + C4 E4 ; # small d + C5 E5 ; # small ye + C6 F4 ; # small f + C7 E3 ; # small g + C8 F5 ; # small kh + C9 E8 ; # small i + CA E9 ; # small j + CB EA ; # small k + CC EB ; # small l + CD EC ; # small m + CE ED ; # small n + CF EE ; # small o + + D0 EF ; # small p + D1 FF ; # small ya + D2 F0 ; # small r + D3 F1 ; # small s + D4 F2 ; # small t + D5 F3 ; # small u + D6 E6 ; # small zh + D7 E2 ; # small v + D8 FC ; # small soft sign + D9 FB ; # small y + DA E7 ; # small z + DB F8 ; # small sh + DC FD ; # small e + DD F9 ; # small shch + DE F7 ; # small ch + DF FA ; # small hard sign + + E0 DE ; # capital YU + E1 C0 ; # capital A + E2 C1 ; # capital B + E3 D6 ; # capital TS + E4 C4 ; # capital D + E5 C5 ; # capital YE + E6 D4 ; # capital F + E7 C3 ; # capital G + E8 D5 ; # capital KH + E9 C8 ; # capital I + EA C9 ; # capital J + EB CA ; # capital K + EC CB ; # capital L + ED CC ; # capital M + EE CD ; # capital N + EF CE ; # capital O + + F0 CF ; # capital P + F1 DF ; # capital YA + F2 D0 ; # capital R + F3 D1 ; # capital S + F4 D2 ; # capital T + F5 D3 ; # capital U + F6 C6 ; # capital ZH + F7 C2 ; # capital V + F8 DC ; # capital soft sign + F9 DB ; # capital Y + FA C7 ; # capital Z + FB D8 ; # capital SH + FC DD ; # capital E + FD D9 ; # capital SHCH + FE D7 ; # capital CH + FF DA ; # capital hard sign +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/map_https_fcgi.conf b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/map_https_fcgi.conf new file mode 100644 index 000000000..a8d62223a --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/map_https_fcgi.conf @@ -0,0 +1,7 @@ +# -*- mode: conf; mode: flyspell-prog; ispell-local-dictionary: "american" -*- +### Implement the $https_if_not_empty variable for Nginx versions below 1.1.11. + +map $scheme $https { + default ''; + https on; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/mime.types b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/mime.types new file mode 100644 index 000000000..618b8f8e7 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/mime.types @@ -0,0 +1,77 @@ +# -*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-current-dictionary: american -*- +types { + text/html html htm shtml; + text/css css; + text/xml xml rss; + image/gif gif; + image/jpeg jpeg jpg; + application/x-javascript js; + application/atom+xml atom; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + image/svg+xml svg svgz; + + application/java-archive jar war ear; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.ms-excel xls; + application/vnd.ms-powerpoint ppt; + application/vnd.wap.wmlc wmlc; + application/vnd.wap.xhtml+xml xhtml; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/zip zip; + + # Mime types for web fonts. Stolen from here: + # http://seconddrawer.com.au/blog/ in part. + application/x-font-ttf ttf; + font/opentype otf; + application/vnd.ms-fontobject eot; + application/x-woff woff; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mpeg mpeg mpg; + video/quicktime mov; + video/x-flv flv; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/nginx.conf b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/nginx.conf new file mode 100644 index 000000000..22ad4c317 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/nginx.conf @@ -0,0 +1,119 @@ +# -*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: "american" -*- +user www-data; +worker_processes 4; + +error_log /var/log/nginx/error.log; +pid /var/run/nginx.pid; + +worker_rlimit_nofile 8192; + +events { + worker_connections 4096; + ## epoll is preferred on 2.6 Linux + ## kernels. Cf. http://www.kegel.com/c10k.html#nb.epoll + use epoll; + ## Accept as many connections as possible. + multi_accept on; +} + +http { + ## MIME types. + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## FastCGI. + include /etc/nginx/fastcgi.conf; + + ## Default log and error files. + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + ## Use sendfile() syscall to speed up I/O operations and speed up + ## static file serving. + sendfile on; + ## Handling of IPs in proxied and load balancing situations. + set_real_ip_from 0.0.0.0/32; # all addresses get a real IP. + real_ip_header X-Forwarded-For; # the ip is forwarded from the load balancer/proxy + + ## Define a zone for limiting the number of simultaneous + ## connections nginx accepts. 1m means 32000 simultaneous + ## sessions. We need to define for each server the limit_conn + ## value refering to this or other zones. + ## ** This syntax requires nginx version >= + ## ** 1.1.8. Cf. http://nginx.org/en/CHANGES. If using an older + ## ** version then use the limit_zone directive below + ## ** instead. Comment out this + ## ** one if not using nginx version >= 1.1.8. + limit_conn_zone $binary_remote_addr zone=arbeit:10m; + + ## Timeouts. + client_body_timeout 60; + client_header_timeout 60; + keepalive_timeout 10 10; + send_timeout 60; + + ## Reset lingering timed out connections. Deflect DDoS. + reset_timedout_connection on; + + ## Body size. + client_max_body_size 10m; + + ## TCP options. + tcp_nodelay on; + tcp_nopush on; + + ## Compression. + gzip on; + gzip_buffers 16 8k; + gzip_comp_level 1; + gzip_http_version 1.1; + gzip_min_length 10; + gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript image/x-icon application/vnd.ms-fontobject font/opentype application/x-font-ttf; + gzip_vary on; + gzip_proxied any; # Compression for all requests. + ## No need for regexps. See + ## http://wiki.nginx.org/NginxHttpGzipModule#gzip_disable + gzip_disable "msie6"; + + ## Serve already compressed files directly, bypassing on-the-fly + ## compression. + gzip_static on; + + ## Hide the Nginx version number. + server_tokens off; + + ## Use a SSL/TLS cache for SSL session resume. This needs to be + ## here (in this context, for session resumption to work. See this + ## thread on the Nginx mailing list: + ## http://nginx.org/pipermail/nginx/2010-November/023736.html. + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + ## For the filefield_nginx_progress module to work. From the + ## README. Reserve 1MB under the name 'uploads' to track uploads. + upload_progress uploads 1m; + + ## Enable clickjacking protection in modern browsers. Available in + ## IE8 also. See + ## https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header + add_header X-Frame-Options sameorigin; + + ## Include the upstream servers for PHP FastCGI handling config. + include upstream_phpcgi.conf; + + ## If using Nginx version >= 1.1.11 then there's a $https variable + ## that has the value 'on' if the used scheme is https and '' if not. + ## See: http://trac.nginx.org/nginx/changeset/4380/nginx + ## http://trac.nginx.org/nginx/changeset/4333/nginx and + ## http://trac.nginx.org/nginx/changeset/4334/nginx. If using a + ## previous version then uncomment out the line below. + #include map_https_fcgi.conf; + + ## Include the upstream servers for Apache handling the PHP + ## processes. In this case Nginx functions as a reverse proxy. + #include reverse_proxy.conf; + #include upstream_phpapache.conf; + + ## Include all vhosts. + include /etc/nginx/sites-enabled/*; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/reverse_proxy.conf b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/reverse_proxy.conf new file mode 100644 index 000000000..ee0faadd7 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/reverse_proxy.conf @@ -0,0 +1,10 @@ +# -*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: "american" -*- + +### Configuration for reverse proxy. Passing the necessary headers to +### the backend. Nginx doesn't tunnel the connection, it opens a new +### one. Hence whe need to send these headers to the backend so that +### the client(s) IP is available to them. The host is also sent. + +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header Host $http_host; diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/000-default b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/000-default new file mode 100644 index 000000000..9dbaa44ff --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/000-default @@ -0,0 +1,19 @@ +# -*-mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: "american" -*- +### Block all illegal host headers. Taken from a discussion on nginx +### forums. Cf. http://forum.nginx.org/read.php?2,3482,3518 following +### a suggestion by Maxim Dounin. Also suggested in +### http://nginx.org/en/docs/http/request_processing.html. +server { + listen [::]:80 default_server; + # Uncomment the line below and comment the above if you're + # running a Nginx version less than 0.8.20. + # listen [::]:80 default; + + # Accept redirects based on the value of the Host header. If + # there's no valid vhost configuration file with a + # corresponding server_name directive then signal an error and + # fail silently. See: + # http://wiki.nginx.org/NginxHttpCoreModule#server_name_in_redirect + server_name_in_redirect off; + return 444; +} diff --git a/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/chive.example.com.conf b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/chive.example.com.conf new file mode 100644 index 000000000..e77024456 --- /dev/null +++ b/certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/chive.example.com.conf @@ -0,0 +1,102 @@ +# -*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: "american" -*- +### Nginx configuration for Chive. + +server { + ## This is to avoid the spurious if for sub-domain name + ## rewriting. See http://wiki.nginx.org/Pitfalls#Server_Name. + listen 80; # IPv4 + + ## Replace the IPv6 address by your own address. The address below + ## was stolen from the wikipedia page on IPv6. + listen [fe80::202:b3ff:fe1e:8329]:80 ipv6only=on; + + server_name www.chive.example.com; + + return 301 $scheme://chive.example.com$request_uri; + +} # server domain rewrite. + +server { + listen 80; # IPv4 + + ## Replace the IPv6 address by your own address. The address below + ## was stolen from the wikipedia page on IPv6. + listen [fe80::202:b3ff:fe1e:8329]:80 ipv6only=on; + + limit_conn arbeit 32; + server_name chive.example.com; + + ## Parameterization using hostname of access and log filenames. + access_log /var/log/nginx/chive.example.com_access.log; + error_log /var/log/nginx/chive.example.com_error.log; + + root /var/www/sites/chive.example.com; + index index.php index.html; + + ## Support for favicon. Return a 204 (No Content) if the favicon + ## doesn't exist. + location = /favicon.ico { + try_files /favicon.ico =204; + } + + ## The main location is accessed using Basic Auth. + location / { + ## Access is restricted. + auth_basic "Restricted Access"; # auth realm + auth_basic_user_file .htpasswd-users; # htpasswd file + + ## Use PATH_INFO for translating the requests to the + ## FastCGI. This config follows Igor's suggestion here: + ## http://forum.nginx.org/read.php?2,124378,124582. + ## This is preferable to using: + ## fastcgi_split_path_info ^(.+\.php)(.*)$ + ## It saves one regex in the location. Hence it's faster. + location ~ ^(?