mirror of
https://github.com/certbot/certbot.git
synced 2026-08-03 00:22:04 +02:00
Merge remote-tracking branch 'origin/master' into nginx-compatibility-test
This commit is contained in:
+1
-1
@@ -39,4 +39,4 @@ Current Features
|
|||||||
|
|
||||||
.. Do not modify this comment unless you know what you're doing. tag:features-end
|
.. Do not modify this comment unless you know what you're doing. tag:features-end
|
||||||
|
|
||||||
For extensive documentation on using and contributing to Certbot, go to https://certbot.eff.org/docs. If you would like to contribute to the project or run the latest code from git, you should read our `developer guide <https://certbot.eff.org/docs/contributing.html>`.
|
For extensive documentation on using and contributing to Certbot, go to https://certbot.eff.org/docs. If you would like to contribute to the project or run the latest code from git, you should read our `developer guide <https://certbot.eff.org/docs/contributing.html>`_.
|
||||||
|
|||||||
+71
-4
@@ -14,7 +14,6 @@ from acme import crypto_util
|
|||||||
from acme import fields
|
from acme import fields
|
||||||
from acme import jose
|
from acme import jose
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -206,6 +205,74 @@ class KeyAuthorizationChallenge(_TokenChallenge):
|
|||||||
self.validation(account_key, *args, **kwargs))
|
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 JWK account_public_key: Public key for the key pair
|
||||||
|
being authorized.
|
||||||
|
|
||||||
|
:returns: ``True`` iff validation with the TXT records resolved from a
|
||||||
|
DNS server is successful.
|
||||||
|
:rtype: bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.verify(chall, account_public_key):
|
||||||
|
logger.debug("Verification of key authorization in response failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
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:
|
||||||
|
from acme import dns_resolver
|
||||||
|
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 "
|
||||||
|
"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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
|
||||||
|
:param unicode name: Domain name being validated.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return "{0}.{1}".format(self.LABEL, name)
|
||||||
|
|
||||||
|
|
||||||
@ChallengeResponse.register
|
@ChallengeResponse.register
|
||||||
class HTTP01Response(KeyAuthorizationChallengeResponse):
|
class HTTP01Response(KeyAuthorizationChallengeResponse):
|
||||||
"""ACME http-01 challenge response."""
|
"""ACME http-01 challenge response."""
|
||||||
@@ -231,8 +298,8 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
|
|||||||
being authorized.
|
being authorized.
|
||||||
:param int port: Port used in the validation.
|
:param int port: Port used in the validation.
|
||||||
|
|
||||||
:returns: ``True`` iff validation is successful, ``False``
|
:returns: ``True`` iff validation with the files currently served by the
|
||||||
otherwise.
|
HTTP server is successful.
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -410,7 +477,7 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse):
|
|||||||
|
|
||||||
|
|
||||||
:returns: ``True`` iff client's control of the domain has been
|
:returns: ``True`` iff client's control of the domain has been
|
||||||
verified, ``False`` otherwise.
|
verified.
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -77,6 +77,93 @@ class KeyAuthorizationChallengeResponseTest(unittest.TestCase):
|
|||||||
self.assertFalse(response.verify(self.chall, KEY.public_key()))
|
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)
|
||||||
|
|
||||||
|
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.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_resolver.assert_called_once_with(
|
||||||
|
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):
|
||||||
|
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(
|
||||||
|
self.chall.validation_domain_name("local"))
|
||||||
|
|
||||||
|
@mock.patch("acme.dns_resolver.txt_records_for_name")
|
||||||
|
def test_simple_verify_bad_validation(self, mock_dns):
|
||||||
|
mock_dns.return_value = ["!"]
|
||||||
|
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(
|
||||||
|
"rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk",
|
||||||
|
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):
|
class HTTP01ResponseTest(unittest.TestCase):
|
||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""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 dns.resolver.NXDOMAIN as error:
|
||||||
|
return []
|
||||||
|
except dns.exception.DNSException as error:
|
||||||
|
logger.error("Error resolving %s: %s", name, str(error))
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [txt_rec.decode("utf-8") for rdata in dns_response
|
||||||
|
for txt_rec in rdata.strings]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Tests for acme.dns_resolver."""
|
||||||
|
import unittest
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from acme import dns_resolver
|
||||||
|
|
||||||
|
try:
|
||||||
|
import dns
|
||||||
|
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):
|
||||||
|
|
||||||
|
@mock.patch("acme.dns_resolver.dns.resolver.query")
|
||||||
|
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 = 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.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'))
|
||||||
|
|
||||||
|
def run(self, result=None):
|
||||||
|
if dns is None: # pragma: no cover
|
||||||
|
print(self, "... SKIPPING, no dnspython available")
|
||||||
|
return
|
||||||
|
super(TxtRecordsForNameTest, self).run(result)
|
||||||
@@ -35,6 +35,11 @@ if sys.version_info < (2, 7):
|
|||||||
else:
|
else:
|
||||||
install_requires.append('mock')
|
install_requires.append('mock')
|
||||||
|
|
||||||
|
# dnspython 1.12 is required to support both Python 2 and Python 3.
|
||||||
|
dns_extras = [
|
||||||
|
'dnspython>=1.12',
|
||||||
|
]
|
||||||
|
|
||||||
dev_extras = [
|
dev_extras = [
|
||||||
'nose',
|
'nose',
|
||||||
'pep8',
|
'pep8',
|
||||||
@@ -76,6 +81,7 @@ setup(
|
|||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
extras_require={
|
extras_require={
|
||||||
|
'dns': dns_extras,
|
||||||
'dev': dev_extras,
|
'dev': dev_extras,
|
||||||
'docs': docs_extras,
|
'docs': docs_extras,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,23 @@
|
|||||||
import pkg_resources
|
import pkg_resources
|
||||||
from certbot import util
|
from certbot import util
|
||||||
|
|
||||||
|
CLI_DEFAULTS_DEFAULT = dict(
|
||||||
|
server_root="/etc/apache2",
|
||||||
|
vhost_root="/etc/apache2/sites-available",
|
||||||
|
vhost_files="*",
|
||||||
|
version_cmd=['apache2ctl', '-v'],
|
||||||
|
define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'],
|
||||||
|
restart_cmd=['apache2ctl', 'graceful'],
|
||||||
|
conftest_cmd=['apache2ctl', 'configtest'],
|
||||||
|
enmod=None,
|
||||||
|
dismod=None,
|
||||||
|
le_vhost_ext="-le-ssl.conf",
|
||||||
|
handle_mods=False,
|
||||||
|
handle_sites=False,
|
||||||
|
challenge_location="/etc/apache2",
|
||||||
|
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
|
||||||
|
"certbot_apache", "options-ssl-apache.conf")
|
||||||
|
)
|
||||||
CLI_DEFAULTS_DEBIAN = dict(
|
CLI_DEFAULTS_DEBIAN = dict(
|
||||||
server_root="/etc/apache2",
|
server_root="/etc/apache2",
|
||||||
vhost_root="/etc/apache2/sites-available",
|
vhost_root="/etc/apache2/sites-available",
|
||||||
@@ -71,7 +87,25 @@ CLI_DEFAULTS_DARWIN = dict(
|
|||||||
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
|
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
|
||||||
"certbot_apache", "options-ssl-apache.conf")
|
"certbot_apache", "options-ssl-apache.conf")
|
||||||
)
|
)
|
||||||
|
CLI_DEFAULTS_SUSE = dict(
|
||||||
|
server_root="/etc/apache2",
|
||||||
|
vhost_root="/etc/apache2/vhosts.d",
|
||||||
|
vhost_files="*.conf",
|
||||||
|
version_cmd=['apache2ctl', '-v'],
|
||||||
|
define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'],
|
||||||
|
restart_cmd=['apache2ctl', 'graceful'],
|
||||||
|
conftest_cmd=['apache2ctl', 'configtest'],
|
||||||
|
enmod="a2enmod",
|
||||||
|
dismod="a2dismod",
|
||||||
|
le_vhost_ext="-le-ssl.conf",
|
||||||
|
handle_mods=False,
|
||||||
|
handle_sites=False,
|
||||||
|
challenge_location="/etc/apache2/vhosts.d",
|
||||||
|
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
|
||||||
|
"certbot_apache", "options-ssl-apache.conf")
|
||||||
|
)
|
||||||
CLI_DEFAULTS = {
|
CLI_DEFAULTS = {
|
||||||
|
"default": CLI_DEFAULTS_DEFAULT,
|
||||||
"debian": CLI_DEFAULTS_DEBIAN,
|
"debian": CLI_DEFAULTS_DEBIAN,
|
||||||
"ubuntu": CLI_DEFAULTS_DEBIAN,
|
"ubuntu": CLI_DEFAULTS_DEBIAN,
|
||||||
"centos": CLI_DEFAULTS_CENTOS,
|
"centos": CLI_DEFAULTS_CENTOS,
|
||||||
@@ -83,6 +117,8 @@ CLI_DEFAULTS = {
|
|||||||
"gentoo": CLI_DEFAULTS_GENTOO,
|
"gentoo": CLI_DEFAULTS_GENTOO,
|
||||||
"gentoo base system": CLI_DEFAULTS_GENTOO,
|
"gentoo base system": CLI_DEFAULTS_GENTOO,
|
||||||
"darwin": CLI_DEFAULTS_DARWIN,
|
"darwin": CLI_DEFAULTS_DARWIN,
|
||||||
|
"opensuse": CLI_DEFAULTS_SUSE,
|
||||||
|
"suse": CLI_DEFAULTS_SUSE,
|
||||||
}
|
}
|
||||||
"""CLI defaults."""
|
"""CLI defaults."""
|
||||||
|
|
||||||
@@ -115,13 +151,36 @@ HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS,
|
|||||||
|
|
||||||
|
|
||||||
def os_constant(key):
|
def os_constant(key):
|
||||||
"""Get a constant value for operating system
|
"""
|
||||||
|
Get a constant value for operating system
|
||||||
|
|
||||||
:param key: name of cli constant
|
:param key: name of cli constant
|
||||||
:return: value of constant for active os
|
:return: value of constant for active os
|
||||||
"""
|
"""
|
||||||
|
|
||||||
os_info = util.get_os_info()
|
os_info = util.get_os_info()
|
||||||
try:
|
try:
|
||||||
constants = CLI_DEFAULTS[os_info[0].lower()]
|
constants = CLI_DEFAULTS[os_info[0].lower()]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
constants = CLI_DEFAULTS["debian"]
|
constants = os_like_constants()
|
||||||
|
if not constants:
|
||||||
|
constants = CLI_DEFAULTS["default"]
|
||||||
return constants[key]
|
return constants[key]
|
||||||
|
|
||||||
|
|
||||||
|
def os_like_constants():
|
||||||
|
"""
|
||||||
|
Try to get constants for distribution with
|
||||||
|
similar layout and configuration, indicated by
|
||||||
|
/etc/os-release variable "LIKE"
|
||||||
|
|
||||||
|
:returns: Constants dictionary
|
||||||
|
:rtype: `dict`
|
||||||
|
"""
|
||||||
|
|
||||||
|
os_like = util.get_systemd_os_like()
|
||||||
|
if os_like:
|
||||||
|
for os_name in os_like:
|
||||||
|
if os_name in CLI_DEFAULTS.keys():
|
||||||
|
return CLI_DEFAULTS[os_name]
|
||||||
|
return {}
|
||||||
|
|||||||
@@ -25,3 +25,20 @@ class ConstantsTest(unittest.TestCase):
|
|||||||
os_info.return_value = ('Nonexistent Linux', '', '')
|
os_info.return_value = ('Nonexistent Linux', '', '')
|
||||||
self.assertEqual(constants.os_constant("vhost_root"),
|
self.assertEqual(constants.os_constant("vhost_root"),
|
||||||
"/etc/apache2/sites-available")
|
"/etc/apache2/sites-available")
|
||||||
|
|
||||||
|
@mock.patch("certbot.util.get_os_info")
|
||||||
|
def test_get_default_constants(self, os_info):
|
||||||
|
os_info.return_value = ('Nonexistent Linux', '', '')
|
||||||
|
with mock.patch("certbot.util.get_systemd_os_like") as os_like:
|
||||||
|
# Get defaults
|
||||||
|
os_like.return_value = False
|
||||||
|
c_hm = constants.os_constant("handle_mods")
|
||||||
|
c_sr = constants.os_constant("server_root")
|
||||||
|
self.assertFalse(c_hm)
|
||||||
|
self.assertEqual(c_sr, "/etc/apache2")
|
||||||
|
# Use darwin as like test target
|
||||||
|
os_like.return_value = ["something", "nonexistent", "darwin"]
|
||||||
|
d_vr = constants.os_constant("vhost_root")
|
||||||
|
d_em = constants.os_constant("enmod")
|
||||||
|
self.assertFalse(d_em)
|
||||||
|
self.assertEqual(d_vr, "/etc/apache2/other")
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""ACME AuthHandler."""
|
"""ACME AuthHandler."""
|
||||||
import itertools
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import six
|
||||||
import zope.component
|
import zope.component
|
||||||
|
|
||||||
from acme import challenges
|
from acme import challenges
|
||||||
@@ -141,7 +141,7 @@ class AuthHandler(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
active_achalls = []
|
active_achalls = []
|
||||||
for achall, resp in itertools.izip(achalls, resps):
|
for achall, resp in six.moves.zip(achalls, resps):
|
||||||
# This line needs to be outside of the if block below to
|
# This line needs to be outside of the if block below to
|
||||||
# ensure failed challenges are cleaned up correctly
|
# ensure failed challenges are cleaned up correctly
|
||||||
active_achalls.append(achall)
|
active_achalls.append(achall)
|
||||||
@@ -472,7 +472,7 @@ def _report_failed_challs(failed_achalls):
|
|||||||
problems.setdefault(achall.error.typ, []).append(achall)
|
problems.setdefault(achall.error.typ, []).append(achall)
|
||||||
|
|
||||||
reporter = zope.component.getUtility(interfaces.IReporter)
|
reporter = zope.component.getUtility(interfaces.IReporter)
|
||||||
for achalls in problems.itervalues():
|
for achalls in six.itervalues(problems):
|
||||||
reporter.add_message(
|
reporter.add_message(
|
||||||
_generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY)
|
_generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY)
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -343,8 +343,10 @@ class HelpfulArgumentParser(object):
|
|||||||
self.determine_verb()
|
self.determine_verb()
|
||||||
help1 = self.prescan_for_flag("-h", self.help_topics)
|
help1 = self.prescan_for_flag("-h", self.help_topics)
|
||||||
help2 = self.prescan_for_flag("--help", self.help_topics)
|
help2 = self.prescan_for_flag("--help", self.help_topics)
|
||||||
assert max(True, "a") == "a", "Gravity changed direction"
|
if isinstance(help1, bool) and isinstance(help2, bool):
|
||||||
self.help_arg = max(help1, help2)
|
self.help_arg = help1 or help2
|
||||||
|
else:
|
||||||
|
self.help_arg = help1 if isinstance(help1, str) else help2
|
||||||
if self.help_arg is True:
|
if self.help_arg is True:
|
||||||
# just --help with no topic; avoid argparse altogether
|
# just --help with no topic; avoid argparse altogether
|
||||||
print(usage)
|
print(usage)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ CLI_DEFAULTS = dict(
|
|||||||
os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"),
|
os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"),
|
||||||
"letsencrypt", "cli.ini"),
|
"letsencrypt", "cli.ini"),
|
||||||
],
|
],
|
||||||
verbose_count=-(logging.INFO / 10),
|
verbose_count=-int(logging.INFO / 10),
|
||||||
server="https://acme-v01.api.letsencrypt.org/directory",
|
server="https://acme-v01.api.letsencrypt.org/directory",
|
||||||
rsa_key_size=2048,
|
rsa_key_size=2048,
|
||||||
rollback_checkpoints=1,
|
rollback_checkpoints=1,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import collections
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
import six
|
||||||
|
|
||||||
import zope.interface
|
import zope.interface
|
||||||
import zope.interface.verify
|
import zope.interface.verify
|
||||||
@@ -194,12 +195,12 @@ class PluginsRegistry(collections.Mapping):
|
|||||||
def init(self, config):
|
def init(self, config):
|
||||||
"""Initialize all plugins in the registry."""
|
"""Initialize all plugins in the registry."""
|
||||||
return [plugin_ep.init(config) for plugin_ep
|
return [plugin_ep.init(config) for plugin_ep
|
||||||
in self._plugins.itervalues()]
|
in six.itervalues(self._plugins)]
|
||||||
|
|
||||||
def filter(self, pred):
|
def filter(self, pred):
|
||||||
"""Filter plugins based on predicate."""
|
"""Filter plugins based on predicate."""
|
||||||
return type(self)(dict((name, plugin_ep) for name, plugin_ep
|
return type(self)(dict((name, plugin_ep) for name, plugin_ep
|
||||||
in self._plugins.iteritems() if pred(plugin_ep)))
|
in six.iteritems(self._plugins) if pred(plugin_ep)))
|
||||||
|
|
||||||
def visible(self):
|
def visible(self):
|
||||||
"""Filter plugins based on visibility."""
|
"""Filter plugins based on visibility."""
|
||||||
@@ -216,7 +217,7 @@ class PluginsRegistry(collections.Mapping):
|
|||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
"""Prepare all plugins in the registry."""
|
"""Prepare all plugins in the registry."""
|
||||||
return [plugin_ep.prepare() for plugin_ep in self._plugins.itervalues()]
|
return [plugin_ep.prepare() for plugin_ep in six.itervalues(self._plugins)]
|
||||||
|
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Filter plugins based on availability."""
|
"""Filter plugins based on availability."""
|
||||||
@@ -238,7 +239,7 @@ class PluginsRegistry(collections.Mapping):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
# use list instead of set because PluginEntryPoint is not hashable
|
# use list instead of set because PluginEntryPoint is not hashable
|
||||||
candidates = [plugin_ep for plugin_ep in self._plugins.itervalues()
|
candidates = [plugin_ep for plugin_ep in six.itervalues(self._plugins)
|
||||||
if plugin_ep.initialized and plugin_ep.init() is plugin]
|
if plugin_ep.initialized and plugin_ep.init() is plugin]
|
||||||
assert len(candidates) <= 1
|
assert len(candidates) <= 1
|
||||||
if candidates:
|
if candidates:
|
||||||
@@ -249,7 +250,7 @@ class PluginsRegistry(collections.Mapping):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "{0}({1})".format(
|
return "{0}({1})".format(
|
||||||
self.__class__.__name__, ','.join(
|
self.__class__.__name__, ','.join(
|
||||||
repr(p_ep) for p_ep in self._plugins.itervalues()))
|
repr(p_ep) for p_ep in six.itervalues(self._plugins)))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if not self._plugins:
|
if not self._plugins:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import six
|
||||||
import zope.component
|
import zope.component
|
||||||
import zope.interface
|
import zope.interface
|
||||||
|
|
||||||
@@ -187,7 +188,7 @@ s.serve_forever()" """
|
|||||||
#answer = zope.component.getUtility(interfaces.IDisplay).notification(
|
#answer = zope.component.getUtility(interfaces.IDisplay).notification(
|
||||||
# message=message, height=25, pause=True)
|
# message=message, height=25, pause=True)
|
||||||
sys.stdout.write(message)
|
sys.stdout.write(message)
|
||||||
raw_input("Press ENTER to continue")
|
six.moves.input("Press ENTER to continue")
|
||||||
|
|
||||||
def cleanup(self, achalls):
|
def cleanup(self, achalls):
|
||||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ def pick_plugin(config, default, plugins, question, ifaces):
|
|||||||
else:
|
else:
|
||||||
return plugin_ep.init()
|
return plugin_ep.init()
|
||||||
elif len(prepared) == 1:
|
elif len(prepared) == 1:
|
||||||
plugin_ep = prepared.values()[0]
|
plugin_ep = list(prepared.values())[0]
|
||||||
logger.debug("Single candidate plugin: %s", plugin_ep)
|
logger.debug("Single candidate plugin: %s", plugin_ep)
|
||||||
if plugin_ep.misconfigured:
|
if plugin_ep.misconfigured:
|
||||||
return None
|
return None
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
NAME="SystemdOS"
|
NAME="SystemdOS"
|
||||||
VERSION="42.42.42 LTS, Unreal"
|
VERSION="42.42.42 LTS, Unreal"
|
||||||
ID=systemdos
|
ID=systemdos
|
||||||
ID_LIKE=debian
|
ID_LIKE="something nonexistent debian"
|
||||||
VERSION_ID="42"
|
VERSION_ID="42"
|
||||||
HOME_URL="http://www.example.com/"
|
HOME_URL="http://www.example.com/"
|
||||||
SUPPORT_URL="http://help.example.com/"
|
SUPPORT_URL="http://help.example.com/"
|
||||||
|
|||||||
@@ -359,6 +359,15 @@ class OsInfoTest(unittest.TestCase):
|
|||||||
with mock.patch('os.path.isfile', return_value=False):
|
with mock.patch('os.path.isfile', return_value=False):
|
||||||
self.assertEqual(get_systemd_os_info(), ("", ""))
|
self.assertEqual(get_systemd_os_info(), ("", ""))
|
||||||
|
|
||||||
|
def test_systemd_os_release_like(self):
|
||||||
|
from certbot.util import get_systemd_os_like
|
||||||
|
|
||||||
|
with mock.patch('os.path.isfile', return_value=True):
|
||||||
|
id_likes = get_systemd_os_like(test_util.vector_path(
|
||||||
|
"os-release"))
|
||||||
|
self.assertEqual(len(id_likes), 3)
|
||||||
|
self.assertTrue("debian" in id_likes)
|
||||||
|
|
||||||
@mock.patch("certbot.util.subprocess.Popen")
|
@mock.patch("certbot.util.subprocess.Popen")
|
||||||
def test_non_systemd_os_info(self, popen_mock):
|
def test_non_systemd_os_info(self, popen_mock):
|
||||||
from certbot.util import (get_os_info, get_python_os_info,
|
from certbot.util import (get_os_info, get_python_os_info,
|
||||||
|
|||||||
@@ -268,6 +268,19 @@ def get_systemd_os_info(filepath="/etc/os-release"):
|
|||||||
return (os_name, os_version)
|
return (os_name, os_version)
|
||||||
|
|
||||||
|
|
||||||
|
def get_systemd_os_like(filepath="/etc/os-release"):
|
||||||
|
"""
|
||||||
|
Get a list of strings that indicate the distribution likeness to
|
||||||
|
other distributions.
|
||||||
|
|
||||||
|
:param str filepath: File path of os-release file
|
||||||
|
:returns: List of distribution acronyms
|
||||||
|
:rtype: `list` of `str`
|
||||||
|
"""
|
||||||
|
|
||||||
|
return _get_systemd_os_release_var("ID_LIKE", filepath).split(" ")
|
||||||
|
|
||||||
|
|
||||||
def _get_systemd_os_release_var(varname, filepath="/etc/os-release"):
|
def _get_systemd_os_release_var(varname, filepath="/etc/os-release"):
|
||||||
"""
|
"""
|
||||||
Get single value from systemd /etc/os-release
|
Get single value from systemd /etc/os-release
|
||||||
@@ -409,6 +422,9 @@ def enforce_domain_sanity(domain):
|
|||||||
else:
|
else:
|
||||||
raise errors.ConfigurationError(str(error_fmt).format(domain))
|
raise errors.ConfigurationError(str(error_fmt).format(domain))
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
domain = domain.decode('ascii')
|
||||||
|
|
||||||
# Remove trailing dot
|
# Remove trailing dot
|
||||||
domain = domain[:-1] if domain.endswith('.') else domain
|
domain = domain[:-1] if domain.endswith('.') else domain
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ Developer Guide
|
|||||||
:local:
|
:local:
|
||||||
|
|
||||||
|
|
||||||
.. _hacking:
|
.. _getting_started:
|
||||||
|
|
||||||
Hacking
|
Getting Started
|
||||||
=======
|
=======
|
||||||
|
|
||||||
Running a local copy of the client
|
Running a local copy of the client
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ install_requires = [
|
|||||||
'parsedatetime>=1.3', # Calendar.parseDT
|
'parsedatetime>=1.3', # Calendar.parseDT
|
||||||
'PyOpenSSL',
|
'PyOpenSSL',
|
||||||
'pyrfc3339',
|
'pyrfc3339',
|
||||||
'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280
|
|
||||||
'pytz',
|
'pytz',
|
||||||
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
|
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
|
||||||
# will tolerate; see #2599:
|
# will tolerate; see #2599:
|
||||||
@@ -52,6 +51,12 @@ install_requires = [
|
|||||||
'zope.interface',
|
'zope.interface',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Debian squeeze support, cf. #280
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
install_requires.append('python2-pythondialog>=3.2.2rc1')
|
||||||
|
else:
|
||||||
|
install_requires.append('pythondialog>=3.2.2rc1')
|
||||||
|
|
||||||
# env markers in extras_require cause problems with older pip: #517
|
# env markers in extras_require cause problems with older pip: #517
|
||||||
# Keep in sync with conditional_requirements.py.
|
# Keep in sync with conditional_requirements.py.
|
||||||
if sys.version_info < (2, 7):
|
if sys.version_info < (2, 7):
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ envlist = py{26,33,34,35},cover,lint
|
|||||||
# packages installed separately to ensure that downstream deps problems
|
# packages installed separately to ensure that downstream deps problems
|
||||||
# are detected, c.f. #1002
|
# are detected, c.f. #1002
|
||||||
commands =
|
commands =
|
||||||
pip install -e acme[dev]
|
pip install -e acme[dns,dev]
|
||||||
nosetests -v acme
|
nosetests -v acme
|
||||||
pip install -e .[dev]
|
pip install -e .[dev]
|
||||||
nosetests -v certbot
|
nosetests -v certbot
|
||||||
@@ -38,23 +38,23 @@ deps =
|
|||||||
|
|
||||||
[testenv:py33]
|
[testenv:py33]
|
||||||
commands =
|
commands =
|
||||||
pip install -e acme[dev]
|
pip install -e acme[dns,dev]
|
||||||
nosetests -v acme
|
nosetests -v acme
|
||||||
|
|
||||||
[testenv:py34]
|
[testenv:py34]
|
||||||
commands =
|
commands =
|
||||||
pip install -e acme[dev]
|
pip install -e acme[dns,dev]
|
||||||
nosetests -v acme
|
nosetests -v acme
|
||||||
|
|
||||||
[testenv:py35]
|
[testenv:py35]
|
||||||
commands =
|
commands =
|
||||||
pip install -e acme[dev]
|
pip install -e acme[dns,dev]
|
||||||
nosetests -v acme
|
nosetests -v acme
|
||||||
|
|
||||||
[testenv:cover]
|
[testenv:cover]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
commands =
|
commands =
|
||||||
pip install -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot
|
pip install -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot
|
||||||
./tox.cover.sh
|
./tox.cover.sh
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
@@ -64,7 +64,7 @@ basepython = python2.7
|
|||||||
# duplicate code checking; if one of the commands fails, others will
|
# duplicate code checking; if one of the commands fails, others will
|
||||||
# continue, but tox return code will reflect previous error
|
# continue, but tox return code will reflect previous error
|
||||||
commands =
|
commands =
|
||||||
pip install -q -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot
|
pip install -q -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e certbot-compatibility-test -e letshelp-certbot
|
||||||
./pep8.travis.sh
|
./pep8.travis.sh
|
||||||
pylint --reports=n --rcfile=.pylintrc certbot
|
pylint --reports=n --rcfile=.pylintrc certbot
|
||||||
pylint --reports=n --rcfile=acme/.pylintrc acme/acme
|
pylint --reports=n --rcfile=acme/.pylintrc acme/acme
|
||||||
|
|||||||
Reference in New Issue
Block a user