From d48c560df13f283c83217d04bb09f81de042e48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6gl?= Date: Sun, 14 Feb 2016 22:21:25 +0100 Subject: [PATCH 01/24] correctly handle IPv6 and IPv4 addresses fix #1143 This commit correctly splits IPv6 addresses into the host and port parts. This will work for normal IPv4 and IPv6 addresses appended by a port number as well es for IPv6 addressess without a port, which should be the normal IPv6 usage. --- letsencrypt/plugins/common.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 37738f5c0..187d84daf 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -110,8 +110,17 @@ class Addr(object): @classmethod def fromstring(cls, str_addr): """Initialize Addr from string.""" - tup = str_addr.partition(':') - return cls((tup[0], tup[2])) + if str_addr.startswith('['): + # ipv6 addresses starts with [ + endIndex = str_addr.rfind(']') + host = str_addr[:endIndex + 1] + port = '' + if len(str_addr) > endIndex + 3 and str_addr[endIndex + 2] == ':': + port = str_addr[endIndex + 3:] + return cls((host, port)) + else: + tup = str_addr.partition(':') + return cls((tup[0], tup[2])) def __str__(self): if self.tup[1]: From b5bd330bd9322310b81856171b5fb0b4ec989be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6gl?= Date: Sun, 14 Feb 2016 23:08:58 +0100 Subject: [PATCH 02/24] add vhost test cases containing IPv6 addresses The two added vhost configuration files should test, whether a reversed order, i.e. first an IPv6 entry followed by an IPv4 one, or an IPv6 adress without a given port works correctly. --- .../{failing => passing}/ipv6-1143.conf | 0 .../{failing => passing}/ipv6-1143b.conf | 0 .../tests/apache-conf-files/passing/ipv6-1143c.conf | 9 +++++++++ .../tests/apache-conf-files/passing/ipv6-1143d.conf | 9 +++++++++ 4 files changed, 18 insertions(+) rename letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/{failing => passing}/ipv6-1143.conf (100%) rename letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/{failing => passing}/ipv6-1143b.conf (100%) create mode 100644 letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf create mode 100644 letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf similarity index 100% rename from letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143.conf rename to letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143b.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf similarity index 100% rename from letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/failing/ipv6-1143b.conf rename to letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf new file mode 100644 index 000000000..f75dd7850 --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf @@ -0,0 +1,9 @@ + +DocumentRoot /xxxx/ +ServerName noodles.net.nz +ServerAlias www.noodles.net.nz +CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined + + AllowOverride All + + diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf new file mode 100644 index 000000000..f16b412da --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf @@ -0,0 +1,9 @@ + +DocumentRoot /xxxx/ +ServerName noodles.net.nz +ServerAlias www.noodles.net.nz +CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined + + AllowOverride All + + From d2a96efa8efb77e9d5423254d8e01bc6a634a112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6gl?= Date: Fri, 19 Feb 2016 23:39:22 +0100 Subject: [PATCH 03/24] add test cases --- letsencrypt/plugins/common_test.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 55319f0a0..5fdd57c5f 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -81,6 +81,9 @@ class AddrTest(unittest.TestCase): self.addr1 = Addr.fromstring("192.168.1.1") self.addr2 = Addr.fromstring("192.168.1.1:*") self.addr3 = Addr.fromstring("192.168.1.1:80") + self.addr4 = Addr.fromstring("[fe00::1]") + self.addr5 = Addr.fromstring("[fe00::1]:*") + self.addr6 = Addr.fromstring("[fe00::1]:80") def test_fromstring(self): self.assertEqual(self.addr1.get_addr(), "192.168.1.1") @@ -89,22 +92,38 @@ class AddrTest(unittest.TestCase): self.assertEqual(self.addr2.get_port(), "*") self.assertEqual(self.addr3.get_addr(), "192.168.1.1") self.assertEqual(self.addr3.get_port(), "80") + self.assertEqual(self.addr4.get_addr(), "[fe00::1]") + self.assertEqual(self.addr4.get_port(), "") + self.assertEqual(self.addr5.get_addr(), "[fe00::1]") + self.assertEqual(self.addr5.get_port(), "*") + self.assertEqual(self.addr6.get_addr(), "[fe00::1]") + self.assertEqual(self.addr6.get_port(), "80") def test_str(self): self.assertEqual(str(self.addr1), "192.168.1.1") self.assertEqual(str(self.addr2), "192.168.1.1:*") self.assertEqual(str(self.addr3), "192.168.1.1:80") + self.assertEqual(str(self.addr4), "[fe00::1]") + self.assertEqual(str(self.addr5), "[fe00::1]:*") + self.assertEqual(str(self.addr6), "[fe00::1]:80") def test_get_addr_obj(self): self.assertEqual(str(self.addr1.get_addr_obj("443")), "192.168.1.1:443") self.assertEqual(str(self.addr2.get_addr_obj("")), "192.168.1.1") self.assertEqual(str(self.addr1.get_addr_obj("*")), "192.168.1.1:*") + self.assertEqual(str(self.addr4.get_addr_obj("443")), "[fe00::1]:443") + self.assertEqual(str(self.addr5.get_addr_obj("")), "[fe00::1]") + self.assertEqual(str(self.addr4.get_addr_obj("*")), "[fe00::1]:*") def test_eq(self): self.assertEqual(self.addr1, self.addr2.get_addr_obj("")) self.assertNotEqual(self.addr1, self.addr2) self.assertFalse(self.addr1 == 3333) + self.assertEqual(self.addr4, self.addr4.get_addr_obj("")) + self.assertNotEqual(self.addr4, self.addr5) + self.assertFalse(self.addr4 == 3333) + def test_set_inclusion(self): from letsencrypt.plugins.common import Addr set_a = set([self.addr1, self.addr2]) @@ -114,6 +133,13 @@ class AddrTest(unittest.TestCase): self.assertEqual(set_a, set_b) + set_c = set([self.addr4, self.addr5]) + addr4b = Addr.fromstring("[fe00::1]") + addr5b = Addr.fromstring("[fe00::1]:*") + set_d = set([addr4b, addr5b]) + + self.assertEqual(set_c, set_d) + class TLSSNI01Test(unittest.TestCase): """Tests for letsencrypt.plugins.common.TLSSNI01.""" From 9b08fd3964e17ca94353f4757946533d41d5edd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6gl?= Date: Fri, 19 Feb 2016 23:40:44 +0100 Subject: [PATCH 04/24] correctly parse ipv6 address This commit fixes the wrong used indexes for parsing the ipv6 address. --- letsencrypt/plugins/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 187d84daf..5a8effc2b 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -115,8 +115,8 @@ class Addr(object): endIndex = str_addr.rfind(']') host = str_addr[:endIndex + 1] port = '' - if len(str_addr) > endIndex + 3 and str_addr[endIndex + 2] == ':': - port = str_addr[endIndex + 3:] + if len(str_addr) > endIndex + 2 and str_addr[endIndex + 1] == ':': + port = str_addr[endIndex + 2:] return cls((host, port)) else: tup = str_addr.partition(':') From d508a47e516d85450a47d3b763e62e3ee1692ee4 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 20 Mar 2016 18:08:53 +0200 Subject: [PATCH 05/24] Added IPv6 address to Apache test data vhost --- .../two_vhost_80/apache2/sites-available/000-default.conf | 2 +- letsencrypt-apache/letsencrypt_apache/tests/util.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf index c759768c5..2bd4e1fe9 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf @@ -1,4 +1,4 @@ - + ServerName ip-172-30-0-17 ServerAdmin webmaster@localhost diff --git a/letsencrypt-apache/letsencrypt_apache/tests/util.py b/letsencrypt-apache/letsencrypt_apache/tests/util.py index fb1e1442d..5bcb6aca6 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/util.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/util.py @@ -133,8 +133,9 @@ def get_vh_truth(temp_dir, config_name): obj.VirtualHost( os.path.join(prefix, "000-default.conf"), os.path.join(aug_pre, "000-default.conf/VirtualHost"), - set([obj.Addr.fromstring("*:80")]), False, True, - "ip-172-30-0-17"), + set([obj.Addr.fromstring("*:80"), + obj.Addr.fromstring("[::]:80")]), + False, True, "ip-172-30-0-17"), obj.VirtualHost( os.path.join(prefix, "letsencrypt.conf"), os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"), From 8fbe7de625eea585384f45be3810363cd099207e Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 20 Mar 2016 18:09:43 +0200 Subject: [PATCH 06/24] Added IPv6 normalization and comparison to Addr object --- letsencrypt/plugins/common.py | 48 +++++++++++++++++++++++++++--- letsencrypt/plugins/common_test.py | 8 +++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index b72c57c7b..0b8ab635a 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -104,8 +104,9 @@ class Addr(object): :param str port: port number or \*, or "" """ - def __init__(self, tup): + def __init__(self, tup, ipv6=False): self.tup = tup + self.ipv6 = ipv6 @classmethod def fromstring(cls, str_addr): @@ -117,7 +118,7 @@ class Addr(object): port = '' if len(str_addr) > endIndex + 2 and str_addr[endIndex + 1] == ':': port = str_addr[endIndex + 2:] - return cls((host, port)) + return cls((host, port), True) else: tup = str_addr.partition(':') return cls((tup[0], tup[2])) @@ -129,7 +130,15 @@ class Addr(object): def __eq__(self, other): if isinstance(other, self.__class__): - return self.tup == other.tup + if self.ipv6: + # import ipdb;ipdb.set_trace() + return (other.ipv6 and + self._normalize_ipv6(self.tup[0]) == + self._normalize_ipv6(other.tup[0]) and + self.tup[1] == other.tup[1]) + else: + return self.tup == other.tup + return False def __hash__(self): @@ -145,7 +154,38 @@ class Addr(object): def get_addr_obj(self, port): """Return new address object with same addr and new port.""" - return self.__class__((self.tup[0], port)) + return self.__class__((self.tup[0], port), self.ipv6) + + def _normalize_ipv6(self, addr): + """Return IPv6 address in normalized form, helper function""" + addr = addr.lstrip("[") + addr = addr.rstrip("]") + return self._explode_ipv6(addr) + + def get_ipv6_exploded(self): + """Return IPv6 in normalized form""" + if self.ipv6: + return ":".join(self._normalize_ipv6(self.tup[0])) + return "" + + def _explode_ipv6(self, addr): + """Explode IPv6 address for comparison""" + result = ['0', '0', '0', '0', '0', '0', '0', '0'] + addr_list = addr.split(":") + append_to_end = False + for i in range(0, len(addr_list)): + block = addr_list[i] + if len(block) == 0: + append_to_end = True + continue + elif len(block) > 1: + # remove trailing zeros + block = block.lstrip("0") + if not append_to_end: + result[i] = str(block) + else: + result[i-len(addr_list)] = str(block) + return result class TLSSNI01(object): diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 5fdd57c5f..9021e7e42 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -98,6 +98,10 @@ class AddrTest(unittest.TestCase): self.assertEqual(self.addr5.get_port(), "*") self.assertEqual(self.addr6.get_addr(), "[fe00::1]") self.assertEqual(self.addr6.get_port(), "80") + self.assertEqual(self.addr6.get_ipv6_exploded(), + "fe00:0:0:0:0:0:0:1") + self.assertEqual(self.addr1.get_ipv6_exploded(), + "") def test_str(self): self.assertEqual(str(self.addr1), "192.168.1.1") @@ -123,6 +127,10 @@ class AddrTest(unittest.TestCase): self.assertEqual(self.addr4, self.addr4.get_addr_obj("")) self.assertNotEqual(self.addr4, self.addr5) self.assertFalse(self.addr4 == 3333) + from letsencrypt.plugins.common import Addr + self.assertEqual(self.addr4, Addr.fromstring("[fe00:0:0::1]")) + self.assertEqual(self.addr4, Addr.fromstring("[fe00:0::0:0:1]")) + def test_set_inclusion(self): from letsencrypt.plugins.common import Addr From 092a1ee0fb8385e247ae45f785112a317183eb16 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 20 Mar 2016 18:37:57 +0200 Subject: [PATCH 07/24] Made apache configs a bit more generic for tests --- .../apache-conf-files/passing/ipv6-1143.conf | 12 +++++----- .../apache-conf-files/passing/ipv6-1143b.conf | 18 +++++++------- .../apache-conf-files/passing/ipv6-1143c.conf | 12 +++++----- .../apache-conf-files/passing/ipv6-1143d.conf | 24 ++++++++++++++----- 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf index ab4ed412e..22b39c9f2 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf @@ -1,9 +1,9 @@ - -DocumentRoot /xxxx/ -ServerName noodles.net.nz -ServerAlias www.noodles.net.nz -CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined - + +DocumentRoot /var/www/html/ +ServerName example.com +ServerAlias www.example.com +CustomLog ${APACHE_LOG_DIR}/example.log combined + AllowOverride All diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf index 25655a07c..4df497ab2 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf @@ -1,10 +1,9 @@ - - -DocumentRoot /xxxx/ -ServerName noodles.net.nz -ServerAlias www.noodles.net.nz -CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined - + +DocumentRoot /var/www/html/ +ServerName example.com +ServerAlias www.example.com +CustomLog ${APACHE_LOG_DIR}/example.log combined + AllowOverride All @@ -14,8 +13,9 @@ CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined SSLProtocol all -SSLv2 -SSLv3 SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS" - SSLCertificateFile /xxxx/noodles.net.nz.crt - SSLCertificateKeyFile /xxxx/noodles.net.nz.key + SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem + SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key + Header set Strict-Transport-Security "max-age=31536000; preload" diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf index f75dd7850..40670b336 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf @@ -1,9 +1,9 @@ - -DocumentRoot /xxxx/ -ServerName noodles.net.nz -ServerAlias www.noodles.net.nz -CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined - + +DocumentRoot /var/www/html/ +ServerName example.com +ServerAlias www.example.com +CustomLog ${APACHE_LOG_DIR}/example.log combined + AllowOverride All diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf index f16b412da..813c41b62 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf @@ -1,9 +1,21 @@ - -DocumentRoot /xxxx/ -ServerName noodles.net.nz -ServerAlias www.noodles.net.nz -CustomLog ${APACHE_LOG_DIR}/domlogs/noodles.log combined - + +DocumentRoot /var/www/html/ +ServerName example.com +ServerAlias www.example.com +CustomLog ${APACHE_LOG_DIR}/example.log combined + AllowOverride All + + SSLEngine on + + SSLHonorCipherOrder On + SSLProtocol all -SSLv2 -SSLv3 + SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS" + + SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem + SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key + + + Header set Strict-Transport-Security "max-age=31536000; preload" From 9f1504eecd54bad05ca60617a9d4f67a0859bfac Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 20 Mar 2016 20:34:54 +0200 Subject: [PATCH 08/24] Trying to please Travis --- .../tests/apache-conf-files/passing/ipv6-1143.conf | 4 ++-- .../tests/apache-conf-files/passing/ipv6-1143b.conf | 7 ++----- .../tests/apache-conf-files/passing/ipv6-1143c.conf | 4 ++-- .../tests/apache-conf-files/passing/ipv6-1143d.conf | 7 ++----- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf index 22b39c9f2..ad988dc05 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143.conf @@ -1,9 +1,9 @@ -DocumentRoot /var/www/html/ +DocumentRoot /tmp ServerName example.com ServerAlias www.example.com CustomLog ${APACHE_LOG_DIR}/example.log combined - + AllowOverride All diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf index 4df497ab2..e2b4fd3da 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143b.conf @@ -1,9 +1,9 @@ -DocumentRoot /var/www/html/ +DocumentRoot /tmp ServerName example.com ServerAlias www.example.com CustomLog ${APACHE_LOG_DIR}/example.log combined - + AllowOverride All @@ -15,7 +15,4 @@ CustomLog ${APACHE_LOG_DIR}/example.log combined SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key - - - Header set Strict-Transport-Security "max-age=31536000; preload" diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf index 40670b336..f2d2ecbea 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143c.conf @@ -1,9 +1,9 @@ -DocumentRoot /var/www/html/ +DocumentRoot /tmp ServerName example.com ServerAlias www.example.com CustomLog ${APACHE_LOG_DIR}/example.log combined - + AllowOverride All diff --git a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf index 813c41b62..f5b7a2b45 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf +++ b/letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/passing/ipv6-1143d.conf @@ -1,9 +1,9 @@ -DocumentRoot /var/www/html/ +DocumentRoot /tmp ServerName example.com ServerAlias www.example.com CustomLog ${APACHE_LOG_DIR}/example.log combined - + AllowOverride All @@ -15,7 +15,4 @@ CustomLog ${APACHE_LOG_DIR}/example.log combined SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key - - - Header set Strict-Transport-Security "max-age=31536000; preload" From 7675d50f4cf2addeb1995db8fcbba0cc53802609 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 20 Mar 2016 22:29:01 +0200 Subject: [PATCH 09/24] Cleaning up debugging leftovers --- letsencrypt/plugins/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 0b8ab635a..9b3bee7ef 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -131,7 +131,6 @@ class Addr(object): def __eq__(self, other): if isinstance(other, self.__class__): if self.ipv6: - # import ipdb;ipdb.set_trace() return (other.ipv6 and self._normalize_ipv6(self.tup[0]) == self._normalize_ipv6(other.tup[0]) and From 432a85cd18f4b876434fd75d4a65f09222c7029a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 21 Mar 2016 16:19:57 -0700 Subject: [PATCH 10/24] Add Ncurses directory_select --- letsencrypt/display/util.py | 24 ++++++++++++++++++++++++ letsencrypt/tests/display/util_test.py | 5 +++++ 2 files changed, 29 insertions(+) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 84049c47c..3913f8bf1 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -11,6 +11,13 @@ from letsencrypt import errors WIDTH = 72 HEIGHT = 20 +DSELECT_HELP = ( + "Use the arrow keys or tab to move between window elements. Space can be " + "used to complete the input path with the selected element in the " + "directory window. Pressing enter will select the currently highlighted " + "button.") +"""Help text on how to use dialog's dselect.""" + # Display exit codes OK = "ok" """Display exit code indicating user acceptance.""" @@ -21,6 +28,7 @@ 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. @@ -36,6 +44,7 @@ def _wrap_lines(msg): fixed_l.append(textwrap.fill(line, 80)) return os.linesep.join(fixed_l) + @zope.interface.implementer(interfaces.IDisplay) class NcursesDisplay(object): """Ncurses-based display.""" @@ -174,6 +183,21 @@ class NcursesDisplay(object): return self.dialog.checklist( message, width=self.width, height=self.height, choices=choices) + def directory_select(self, message, **unused_kwargs): + """Display a directory selection screen. + + :param str message: prompt to give the user + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + root_directory = os.path.abspath(os.sep) + return self.dialog.dselect( + filepath=root_directory, width=self.width, + height=self.height, help_button=True, title=message) + @zope.interface.implementer(interfaces.IDisplay) class FileDisplay(object): diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index a16eb544e..45025d7a0 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -123,6 +123,11 @@ class NcursesDisplayTest(unittest.TestCase): "message", width=display_util.WIDTH, height=display_util.HEIGHT, choices=choices) + @mock.patch("letsencrypt.display.util.dialog.Dialog.dselect") + def test_directory_select(self, mock_dselect): + self.displayer.directory_select("message") + self.assertEqual(mock_dselect.call_count, 1) + class FileOutputDisplayTest(unittest.TestCase): """Test stdout display. From 08232eef43680e189aaf2776f0953c2a22bd5dfe Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 21 Mar 2016 16:22:15 -0700 Subject: [PATCH 11/24] display.util pep8 cleanup --- letsencrypt/display/util.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 3913f8bf1..05e280118 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -127,7 +127,6 @@ class NcursesDisplay(object): return code, int(index) - 1 - def input(self, message, **unused_kwargs): """Display an input box to the user. @@ -141,11 +140,10 @@ class NcursesDisplay(object): """ 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] + 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) - def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs): """Display a Yes/No dialog box. @@ -397,7 +395,6 @@ class FileDisplay(object): self.outfile.write(side_frame) - def _get_valid_int_ans(self, max_): """Get a numerical selection. @@ -433,6 +430,7 @@ class FileDisplay(object): return OK, selection + @zope.interface.implementer(interfaces.IDisplay) class NoninteractiveDisplay(object): """An iDisplay implementation that never asks for interactive user input""" @@ -507,7 +505,6 @@ class NoninteractiveDisplay(object): else: return OK, default - def yesno(self, message, yes_label=None, no_label=None, default=None, cli_flag=None): # pylint: disable=unused-argument """Decide Yes or No, without asking anybody From 95a4a2ca6098f23322f7ed8e30c218d198e52903 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 21 Mar 2016 16:23:21 -0700 Subject: [PATCH 12/24] display.util_test cleanup --- letsencrypt/tests/display/util_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index 45025d7a0..adec265dc 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -285,6 +285,7 @@ class FileOutputDisplayTest(unittest.TestCase): self.displayer._get_valid_int_ans(3), (display_util.CANCEL, -1)) + class NoninteractiveDisplayTest(unittest.TestCase): """Test non-interactive display. From 0fd453fd5e16c08bed271010539be61d874aa131 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 22 Mar 2016 10:23:14 +0200 Subject: [PATCH 13/24] Added code comments to clarify inner workings --- letsencrypt/plugins/common.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 9b3bee7ef..79c95e149 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -131,6 +131,8 @@ class Addr(object): def __eq__(self, other): if isinstance(other, self.__class__): if self.ipv6: + # compare normalized to take different + # styles of representation into account return (other.ipv6 and self._normalize_ipv6(self.tup[0]) == self._normalize_ipv6(other.tup[0]) and @@ -175,14 +177,17 @@ class Addr(object): for i in range(0, len(addr_list)): block = addr_list[i] if len(block) == 0: + # encountered ::, so rest of the blocks should be + # appended to the end append_to_end = True continue elif len(block) > 1: - # remove trailing zeros + # remove leading zeros block = block.lstrip("0") if not append_to_end: result[i] = str(block) else: + # count the location from the end using negative indices result[i-len(addr_list)] = str(block) return result From 74a7d2bed90a3ecadf73f723f913c6a6e4f0553b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 22 Mar 2016 18:07:51 -0700 Subject: [PATCH 14/24] Added completer.py and tests for FileDisplay --- letsencrypt/display/completer.py | 51 ++++++++++++++++ letsencrypt/tests/display/completer_test.py | 64 +++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 letsencrypt/display/completer.py create mode 100644 letsencrypt/tests/display/completer_test.py diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py new file mode 100644 index 000000000..5258ef52e --- /dev/null +++ b/letsencrypt/display/completer.py @@ -0,0 +1,51 @@ +"""Provides tab autocompletion when prompting users for a path.""" +import glob +import readline + + +class Completer(object): + """Provides tab autocompletion when prompting users for a path. + + This class is meant to be used with readline to provide tab + autocompletion for users entering paths. The complete method can + be passed to readline.set_completer directly, however, this function + works best as a context manager. For example: + + with Completer(): + raw_input() + + In this example, tab autocompletion will be available during + the call to raw_input above, however, readline will be restored to + its previous state when exiting the body of the with statement. + + """ + + def __init__(self): + self._completer = self._delims = self._iter = None + + def complete(self, text, state): + """Provides path autocompletion for use with readline. + + :param str text: text to offer completions for + :param int state: which completion to return + + :returns: possible completion for text or ``None`` if all + completions have been returned + :rtype: str + + """ + if state == 0: + self._iter = glob.iglob(text + '*') + return next(self._iter, None) + + def __enter__(self): + self._completer = readline.get_completer() + self._delims = readline.get_completer_delims() + + readline.set_completer(self.complete) + readline.set_completer_delims(' \t\n;') + readline.parse_and_bind('tab: complete') + + def __exit__(self, unused_type, unused_value, unused_traceback): + readline.set_completer_delims(self._delims) + readline.set_completer(self._completer) diff --git a/letsencrypt/tests/display/completer_test.py b/letsencrypt/tests/display/completer_test.py new file mode 100644 index 000000000..dbba15106 --- /dev/null +++ b/letsencrypt/tests/display/completer_test.py @@ -0,0 +1,64 @@ +"""Test letsencrypt.display.completer.""" +import os +import readline +import shutil +import string +import tempfile +import unittest + + +class CompleterTest(unittest.TestCase): + """Test letsencrypt.display.completer.Completer.""" + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + + # directories must end with os.sep for completer to + # search inside the directory for possible completions + if self.temp_dir[-1] != os.sep: + self.temp_dir += os.sep + + self.paths = [] + # create some files and directories in temp_dir + for c in string.ascii_lowercase: + path = os.path.join(self.temp_dir, c) + self.paths.append(path) + if ord(c) % 2: + os.mkdir(path) + else: + with open(path, 'w'): + pass + + def tearDown(self): + shutil.rmtree(self.temp_dir) + + def test_context_manager(self): + from letsencrypt.display import completer + + original_completer = readline.get_completer() + original_delims = readline.get_completer_delims() + + with completer.Completer(): + pass + + self.assertEqual(readline.get_completer(), original_completer) + self.assertEqual(readline.get_completer_delims(), original_delims) + + def test_complete(self): + from letsencrypt.display import completer + + my_completer = completer.Completer() + num_paths = len(self.paths) + + for i in range(num_paths): + completion = my_completer.complete(self.temp_dir, i) + self.assertTrue(completion in self.paths) + self.paths.remove(completion) + + self.assertFalse(self.paths) + completion = my_completer.complete(self.temp_dir, num_paths) + self.assertEqual(completion, None) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover From 49fefb08dd126f881d33f8c32ec5caf78acf0e2c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 10:35:38 -0700 Subject: [PATCH 15/24] autocomplete -> complete --- letsencrypt/display/completer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 5258ef52e..71e72b942 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -1,22 +1,22 @@ -"""Provides tab autocompletion when prompting users for a path.""" +"""Provides tab completion when prompting users for a path.""" import glob import readline class Completer(object): - """Provides tab autocompletion when prompting users for a path. + """Provides tab completion when prompting users for a path. This class is meant to be used with readline to provide tab - autocompletion for users entering paths. The complete method can - be passed to readline.set_completer directly, however, this function + completion for users entering paths. The complete method can be + passed to readline.set_completer directly, however, this function works best as a context manager. For example: with Completer(): raw_input() - In this example, tab autocompletion will be available during - the call to raw_input above, however, readline will be restored to - its previous state when exiting the body of the with statement. + In this example, tab completion will be available during the call to + raw_input above, however, readline will be restored to its previous + state when exiting the body of the with statement. """ @@ -24,7 +24,7 @@ class Completer(object): self._completer = self._delims = self._iter = None def complete(self, text, state): - """Provides path autocompletion for use with readline. + """Provides path completion for use with readline. :param str text: text to offer completions for :param int state: which completion to return From f9ac7d789b898353a79e74a07d93f71e63d362ee Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 11:15:37 -0700 Subject: [PATCH 16/24] Add support libedit readline --- letsencrypt/display/completer.py | 8 +++- letsencrypt/tests/display/completer_test.py | 52 ++++++++++++++++----- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 71e72b942..6b07a2e47 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -44,7 +44,13 @@ class Completer(object): readline.set_completer(self.complete) readline.set_completer_delims(' \t\n;') - readline.parse_and_bind('tab: complete') + + # readline can be implemented using GNU readline or libedit + # which have different configuration syntax + if 'libedit' in readline.__doc__: + readline.parse_and_bind('bind ^I rl_complete') + else: + readline.parse_and_bind('tab: complete') def __exit__(self, unused_type, unused_value, unused_traceback): readline.set_completer_delims(self._delims) diff --git a/letsencrypt/tests/display/completer_test.py b/letsencrypt/tests/display/completer_test.py index dbba15106..a77d3c842 100644 --- a/letsencrypt/tests/display/completer_test.py +++ b/letsencrypt/tests/display/completer_test.py @@ -6,6 +6,8 @@ import string import tempfile import unittest +import mock + class CompleterTest(unittest.TestCase): """Test letsencrypt.display.completer.Completer.""" @@ -32,18 +34,6 @@ class CompleterTest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.temp_dir) - def test_context_manager(self): - from letsencrypt.display import completer - - original_completer = readline.get_completer() - original_delims = readline.get_completer_delims() - - with completer.Completer(): - pass - - self.assertEqual(readline.get_completer(), original_completer) - self.assertEqual(readline.get_completer_delims(), original_delims) - def test_complete(self): from letsencrypt.display import completer @@ -59,6 +49,44 @@ class CompleterTest(unittest.TestCase): completion = my_completer.complete(self.temp_dir, num_paths) self.assertEqual(completion, None) + def test_context_manager(self): + from letsencrypt.display import completer + + original_completer = readline.get_completer() + original_delims = readline.get_completer_delims() + + with completer.Completer(): + pass + + self.assertEqual(readline.get_completer(), original_completer) + self.assertEqual(readline.get_completer_delims(), original_delims) + + @mock.patch('letsencrypt.display.completer.readline', autospec=True) + def test_context_manager_libedit(self, mock_readline): + mock_readline.__doc__ = 'libedit' + self._test_mocked_readline(mock_readline) + + @mock.patch('letsencrypt.display.completer.readline', autospec=True) + def test_context_manager_readline(self, mock_readline): + mock_readline.__doc__ = 'GNU readline' + self._test_mocked_readline(mock_readline) + + def _test_mocked_readline(self, mock_readline): + from letsencrypt.display import completer + + mock_readline.parse_and_bind.side_effect = enable_tab_completion + + with completer.Completer(): + pass + + self.assertTrue(mock_readline.parse_and_bind.called) + + +def enable_tab_completion(unused_command): + """Enables readline tab completion using the system specific syntax.""" + libedit = 'libedit' in readline.__doc__ + command = 'bind ^I rl_complete' if libedit else 'tab: complete' + readline.parse_and_bind(command) if __name__ == "__main__": unittest.main() # pragma: no cover From 0c0687ca68ae4b9c4fa2585bfd1a974d121bed30 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 11:27:20 -0700 Subject: [PATCH 17/24] Add dummy_readline module --- letsencrypt/display/dummy_readline.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 letsencrypt/display/dummy_readline.py diff --git a/letsencrypt/display/dummy_readline.py b/letsencrypt/display/dummy_readline.py new file mode 100644 index 000000000..fb3d807bb --- /dev/null +++ b/letsencrypt/display/dummy_readline.py @@ -0,0 +1,21 @@ +"""A dummy module with no effect for use on systems without readline.""" + + +def get_completer(): + """An empty implementation of readline.get_completer.""" + + +def get_completer_delims(): + """An empty implementation of readline.get_completer_delims.""" + + +def parse_and_bind(unused_command): + """An empty implementation of readline.parse_and_bind.""" + + +def set_completer(unused_function=None): + """An empty implementation of readline.set_completer.""" + + +def set_completer_delims(unused_delims): + """An empty implementation of readline.set_completer_delims.""" From 7820c687f16550c7a406209918092cfbbd9c53de Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 13:33:07 -0700 Subject: [PATCH 18/24] Use dummy_readline to prevent ImportErrors from readline breaking LE --- letsencrypt/display/completer.py | 6 +++++- letsencrypt/tests/display/completer_test.py | 20 +++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 6b07a2e47..83dafa25d 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -1,6 +1,10 @@ """Provides tab completion when prompting users for a path.""" import glob -import readline +# readline module is not available on all systems +try: + import readline +except ImportError: + import letsencrypt.display.dummy_readline as readline class Completer(object): diff --git a/letsencrypt/tests/display/completer_test.py b/letsencrypt/tests/display/completer_test.py index a77d3c842..3c181c925 100644 --- a/letsencrypt/tests/display/completer_test.py +++ b/letsencrypt/tests/display/completer_test.py @@ -3,10 +3,12 @@ import os import readline import shutil import string +import sys import tempfile import unittest import mock +from six.moves import reload_module # pylint: disable=import-error class CompleterTest(unittest.TestCase): @@ -36,7 +38,6 @@ class CompleterTest(unittest.TestCase): def test_complete(self): from letsencrypt.display import completer - my_completer = completer.Completer() num_paths = len(self.paths) @@ -49,8 +50,17 @@ class CompleterTest(unittest.TestCase): completion = my_completer.complete(self.temp_dir, num_paths) self.assertEqual(completion, None) - def test_context_manager(self): + def test_import_error(self): + original_readline = sys.modules['readline'] + sys.modules['readline'] = None + + self.test_context_manager_with_unmocked_readline() + + sys.modules['readline'] = original_readline + + def test_context_manager_with_unmocked_readline(self): from letsencrypt.display import completer + reload_module(completer) original_completer = readline.get_completer() original_delims = readline.get_completer_delims() @@ -64,14 +74,14 @@ class CompleterTest(unittest.TestCase): @mock.patch('letsencrypt.display.completer.readline', autospec=True) def test_context_manager_libedit(self, mock_readline): mock_readline.__doc__ = 'libedit' - self._test_mocked_readline(mock_readline) + self._test_context_manager_with_mock_readline(mock_readline) @mock.patch('letsencrypt.display.completer.readline', autospec=True) def test_context_manager_readline(self, mock_readline): mock_readline.__doc__ = 'GNU readline' - self._test_mocked_readline(mock_readline) + self._test_context_manager_with_mock_readline(mock_readline) - def _test_mocked_readline(self, mock_readline): + def _test_context_manager_with_mock_readline(self, mock_readline): from letsencrypt.display import completer mock_readline.parse_and_bind.side_effect = enable_tab_completion From 9d70c4acfbc78fda0a2073f2d157a8e8bd9de84b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 15:11:36 -0700 Subject: [PATCH 19/24] Add directory_select method to FileDisplay --- letsencrypt/display/util.py | 14 ++++++++++++++ letsencrypt/tests/display/util_test.py | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 05e280118..c94b0c921 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -7,6 +7,7 @@ import zope.interface from letsencrypt import interfaces from letsencrypt import errors +from letsencrypt.display import completer WIDTH = 72 HEIGHT = 20 @@ -339,6 +340,19 @@ class FileDisplay(object): else: return code, [] + def directory_select(self, message, **unused_kwargs): + """Display a directory selection screen. + + :param str message: prompt to give the user + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + with completer.Completer(): + return self.input(message) + def _scrub_checklist_input(self, indices, tags): # pylint: disable=no-self-use """Validate input and transform indices to appropriate tags. diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index adec265dc..32783b3bd 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -232,6 +232,15 @@ class FileOutputDisplayTest(unittest.TestCase): self.displayer._scrub_checklist_input(list_, TAGS)) self.assertEqual(set_tags, exp[i]) + @mock.patch("letsencrypt.display.util.FileDisplay.input") + def test_directory_select(self, mock_input): + message = "msg" + result = (display_util.OK, "/var/www/html",) + mock_input.return_value = result + + self.assertEqual(self.displayer.directory_select(message), result) + mock_input.assert_called_once_with(message) + def test_scrub_checklist_input_invalid(self): # pylint: disable=protected-access indices = [ From 294ea4d1a64be7be394854af177d35c60c2f1184 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 15:29:02 -0700 Subject: [PATCH 20/24] Add directory_select to NoninteractiveDisplay --- letsencrypt/display/util.py | 17 +++++++++++++++++ letsencrypt/tests/display/util_test.py | 9 +++++++++ 2 files changed, 26 insertions(+) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index c94b0c921..005e2ba9c 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -555,6 +555,23 @@ class NoninteractiveDisplay(object): else: return OK, default + def directory_select(self, message, default=None, cli_flag=None): + """Simulate prompting the user for a directory. + + This function returns default if it is not ``None``, otherwise, + an exception is raised. + + :param str message: prompt to give the user + :param default: default value to return (if one exists) + :param str cli_flag: option used to set this value with the CLI + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + return self.input(message, default, cli_flag) + def separate_list_input(input_): """Separate a comma or space separated list. diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index 32783b3bd..bae0d582a 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -335,6 +335,15 @@ class NoninteractiveDisplayTest(unittest.TestCase): self.assertEqual(ret, (display_util.OK, d)) self.assertRaises(errors.MissingCommandlineFlag, self.displayer.checklist, "message", TAGS) + def test_directory_select(self): + default = "/var/www/html" + expected = (display_util.OK, default) + actual = self.displayer.directory_select("msg", default) + self.assertEqual(expected, actual) + + self.assertRaises( + errors.MissingCommandlineFlag, self.displayer.directory_select, "msg") + class SeparateListInputTest(unittest.TestCase): """Test Module functions.""" From 3031968ac5e148a857e057485f31dbbdbdbe0f39 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 15:32:53 -0700 Subject: [PATCH 21/24] Add directory_select to IDisplay interface --- letsencrypt/interfaces.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 1921b1e54..188a4d9da 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -443,6 +443,20 @@ class IDisplay(zope.interface.Interface): """ + def directory_select(self, message, default=None, cli_flag=None): + """Display a directory selection screen. + + :param str message: prompt to give the user + :param default: the default value to return, if one exists, when + using the NoninteractiveDisplay + :param str cli_flag: option used to set this value with the CLI + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + class IValidator(zope.interface.Interface): """Configuration validator.""" From 2927746c0e8ea8163809d7b3ddd62ea375ab342d Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 25 Mar 2016 17:48:13 +0200 Subject: [PATCH 22/24] Added important check for IPv6 address and tests, improved readability --- letsencrypt/plugins/common.py | 5 ++++- letsencrypt/plugins/common_test.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 707ed7e57..a9410d514 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -118,7 +118,7 @@ class Addr(object): port = '' if len(str_addr) > endIndex + 2 and str_addr[endIndex + 1] == ':': port = str_addr[endIndex + 2:] - return cls((host, port), True) + return cls((host, port), ipv6=True) else: tup = str_addr.partition(':') return cls((tup[0], tup[2])) @@ -173,6 +173,9 @@ class Addr(object): """Explode IPv6 address for comparison""" result = ['0', '0', '0', '0', '0', '0', '0', '0'] addr_list = addr.split(":") + if len(addr_list) > len(result): + # too long, truncate + addr_list = addr_list[0:len(result)] append_to_end = False for i in range(0, len(addr_list)): block = addr_list[i] diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 9021e7e42..292248b90 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -84,6 +84,8 @@ class AddrTest(unittest.TestCase): self.addr4 = Addr.fromstring("[fe00::1]") self.addr5 = Addr.fromstring("[fe00::1]:*") self.addr6 = Addr.fromstring("[fe00::1]:80") + self.addr7 = Addr.fromstring("[fe00::1]:5") + self.addr8 = Addr.fromstring("[fe00:1:2:3:4:5:6:7:8:9]:8080") def test_fromstring(self): self.assertEqual(self.addr1.get_addr(), "192.168.1.1") @@ -102,6 +104,9 @@ class AddrTest(unittest.TestCase): "fe00:0:0:0:0:0:0:1") self.assertEqual(self.addr1.get_ipv6_exploded(), "") + self.assertEqual(self.addr7.get_port(), "5") + self.assertEqual(self.addr7.get_ipv6_exploded(), + "fe00:1:2:3:4:5:6:7") def test_str(self): self.assertEqual(str(self.addr1), "192.168.1.1") From f2fc79fc55e92abb2d19c7f1491af8f1aefe64cc Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 25 Mar 2016 19:12:48 +0200 Subject: [PATCH 23/24] Fixed test case --- letsencrypt/plugins/common_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 292248b90..a4292151e 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -105,7 +105,7 @@ class AddrTest(unittest.TestCase): self.assertEqual(self.addr1.get_ipv6_exploded(), "") self.assertEqual(self.addr7.get_port(), "5") - self.assertEqual(self.addr7.get_ipv6_exploded(), + self.assertEqual(self.addr8.get_ipv6_exploded(), "fe00:1:2:3:4:5:6:7") def test_str(self): From f520ca25565429141d44ed28e3b1fda705ea9682 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 25 Mar 2016 12:52:37 -0700 Subject: [PATCH 24/24] Address @schoen's review comments --- letsencrypt/display/completer.py | 18 +++++++++--------- letsencrypt/display/util.py | 6 ++++-- letsencrypt/interfaces.py | 4 +++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 83dafa25d..fed476bb3 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -1,4 +1,4 @@ -"""Provides tab completion when prompting users for a path.""" +"""Provides Tab completion when prompting users for a path.""" import glob # readline module is not available on all systems try: @@ -8,9 +8,9 @@ except ImportError: class Completer(object): - """Provides tab completion when prompting users for a path. + """Provides Tab completion when prompting users for a path. - This class is meant to be used with readline to provide tab + This class is meant to be used with readline to provide Tab completion for users entering paths. The complete method can be passed to readline.set_completer directly, however, this function works best as a context manager. For example: @@ -18,14 +18,14 @@ class Completer(object): with Completer(): raw_input() - In this example, tab completion will be available during the call to + In this example, Tab completion will be available during the call to raw_input above, however, readline will be restored to its previous state when exiting the body of the with statement. """ def __init__(self): - self._completer = self._delims = self._iter = None + self._iter = self._original_completer = self._original_delims = None def complete(self, text, state): """Provides path completion for use with readline. @@ -43,8 +43,8 @@ class Completer(object): return next(self._iter, None) def __enter__(self): - self._completer = readline.get_completer() - self._delims = readline.get_completer_delims() + self._original_completer = readline.get_completer() + self._original_delims = readline.get_completer_delims() readline.set_completer(self.complete) readline.set_completer_delims(' \t\n;') @@ -57,5 +57,5 @@ class Completer(object): readline.parse_and_bind('tab: complete') def __exit__(self, unused_type, unused_value, unused_traceback): - readline.set_completer_delims(self._delims) - readline.set_completer(self._completer) + readline.set_completer_delims(self._original_delims) + readline.set_completer(self._original_completer) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 005e2ba9c..20c6be156 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -13,7 +13,7 @@ WIDTH = 72 HEIGHT = 20 DSELECT_HELP = ( - "Use the arrow keys or tab to move between window elements. Space can be " + "Use the arrow keys or Tab to move between window elements. Space can be " "used to complete the input path with the selected element in the " "directory window. Pressing enter will select the currently highlighted " "button.") @@ -559,7 +559,9 @@ class NoninteractiveDisplay(object): """Simulate prompting the user for a directory. This function returns default if it is not ``None``, otherwise, - an exception is raised. + an exception is raised explaining the problem. If cli_flag is + not ``None``, the error message will include the flag that can + be used to set this value with the CLI. :param str message: prompt to give the user :param default: default value to return (if one exists) diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 188a4d9da..2fba11869 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -449,7 +449,9 @@ class IDisplay(zope.interface.Interface): :param str message: prompt to give the user :param default: the default value to return, if one exists, when using the NoninteractiveDisplay - :param str cli_flag: option used to set this value with the CLI + :param str cli_flag: option used to set this value with the CLI, + if one exists, to be included in error messages given by + NoninteractiveDisplay :returns: tuple of the form (`code`, `string`) where `code` - int display exit code