Remove optional dependencies (#4088)

* Stop using already_listening in standalone

* remove already_listening

* remove psutil entirely

* fix #595

* Add basic perform test

* make pep8 happy

* Add test_perform_eacces

* add _setup_perform_error

* Add test_perform_unexpected_socket_error

* add test_perform_eaddrinuse_no_retry

* add test_perform_eaddrinuse_retry

* cleanup tests

* stop using dnspython

* don't install dns extras in tox

* remove dns extras from setup.py

* Add simple_verify back to DNS response

* remove dnspython from oldest tests
This commit is contained in:
Brad Warren
2017-01-30 16:55:54 -08:00
committed by GitHub
parent 240438eec7
commit be5bcfe463
15 changed files with 122 additions and 675 deletions
+9 -22
View File
@@ -9,7 +9,6 @@ from cryptography.hazmat.primitives import hashes
import OpenSSL import OpenSSL
import requests import requests
from acme import dns_resolver
from acme import errors from acme import errors
from acme import crypto_util from acme import crypto_util
from acme import fields from acme import fields
@@ -214,36 +213,24 @@ class DNS01Response(KeyAuthorizationChallengeResponse):
def simple_verify(self, chall, domain, account_public_key): def simple_verify(self, chall, domain, account_public_key):
"""Simple verify. """Simple verify.
This method no longer checks DNS records and is a simple wrapper
around `KeyAuthorizationChallengeResponse.verify`.
:param challenges.DNS01 chall: Corresponding challenge. :param challenges.DNS01 chall: Corresponding challenge.
:param unicode domain: Domain name being verified. :param unicode domain: Domain name being verified.
:param JWK account_public_key: Public key for the key pair :param JWK account_public_key: Public key for the key pair
being authorized. being authorized.
:returns: ``True`` iff validation with the TXT records resolved from a :return: ``True`` iff verification of the key authorization was
DNS server is successful. successful.
:rtype: bool :rtype: bool
""" """
if not self.verify(chall, account_public_key): # pylint: disable=unused-argument
verified = self.verify(chall, account_public_key)
if not verified:
logger.debug("Verification of key authorization in response failed") logger.debug("Verification of key authorization in response failed")
return False return verified
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:
txt_records = dns_resolver.txt_records_for_name(
validation_domain_name)
except errors.DependencyError:
raise errors.DependencyError("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 "
"any DNS response in %r", self.key_authorization,
txt_records)
return exists
@Challenge.register # pylint: disable=too-many-ancestors @Challenge.register # pylint: disable=too-many-ancestors
+8 -39
View File
@@ -10,7 +10,6 @@ from six.moves.urllib import parse as urllib_parse # pylint: disable=import-err
from acme import errors from acme import errors
from acme import jose from acme import jose
from acme import test_util from acme import test_util
from acme.dns_resolver import DNS_REQUIREMENT
CERT = test_util.load_comparable_cert('cert.pem') CERT = test_util.load_comparable_cert('cert.pem')
KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem'))
@@ -92,7 +91,6 @@ class DNS01ResponseTest(unittest.TestCase):
from acme.challenges import DNS01 from acme.challenges import DNS01
self.chall = DNS01(token=(b'x' * 16)) self.chall = DNS01(token=(b'x' * 16))
self.response = self.chall.response(KEY) self.response = self.chall.response(KEY)
self.records_for_name_path = "acme.dns_resolver.txt_records_for_name"
def test_to_partial_json(self): def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json()) self.assertEqual(self.jmsg, self.msg.to_partial_json())
@@ -105,45 +103,16 @@ class DNS01ResponseTest(unittest.TestCase):
from acme.challenges import DNS01Response from acme.challenges import DNS01Response
hash(DNS01Response.from_json(self.jmsg)) hash(DNS01Response.from_json(self.jmsg))
def test_simple_verify_bad_key_authorization(self): def test_simple_verify_failure(self):
key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
self.response.simple_verify(self.chall, "local", key2.public_key()) public_key = key2.public_key()
verified = self.response.simple_verify(self.chall, "local", public_key)
self.assertFalse(verified)
@mock.patch('acme.dns_resolver.DNS_AVAILABLE', False) def test_simple_verify_success(self):
def test_simple_verify_without_dns(self): public_key = KEY.public_key()
self.assertRaises( verified = self.response.simple_verify(self.chall, "local", public_key)
errors.DependencyError, self.response.simple_verify, self.assertTrue(verified)
self.chall, 'local', KEY.public_key())
@test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT),
"optional dependency dnspython is not available")
def test_simple_verify_good_validation(self): # pragma: no cover
with mock.patch(self.records_for_name_path) as 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"))
@test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT),
"optional dependency dnspython is not available")
def test_simple_verify_good_validation_multitxts(self): # pragma: no cover
with mock.patch(self.records_for_name_path) as 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"))
@test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT),
"optional dependency dnspython is not available")
def test_simple_verify_bad_validation(self): # pragma: no cover
with mock.patch(self.records_for_name_path) as mock_resolver:
mock_resolver.return_value = ["!"]
self.assertFalse(self.response.simple_verify(
self.chall, "local", KEY.public_key()))
class DNS01Test(unittest.TestCase): class DNS01Test(unittest.TestCase):
-45
View File
@@ -1,45 +0,0 @@
"""DNS Resolver for ACME client.
Required only for local validation of 'dns-01' challenges.
"""
import logging
from acme import errors
from acme import util
DNS_REQUIREMENT = 'dnspython>=1.12'
try:
util.activate(DNS_REQUIREMENT)
# pragma: no cover
import dns.exception
import dns.resolver
DNS_AVAILABLE = True
except errors.DependencyError: # pragma: no cover
DNS_AVAILABLE = False
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
"""
if not DNS_AVAILABLE:
raise errors.DependencyError(
'{0} is required to use this function'.format(DNS_REQUIREMENT))
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]
-77
View File
@@ -1,77 +0,0 @@
"""Tests for acme.dns_resolver."""
import unittest
import mock
from six.moves import reload_module # pylint: disable=import-error
from acme import errors
from acme import test_util
from acme.dns_resolver import DNS_REQUIREMENT
if test_util.requirement_available(DNS_REQUIREMENT):
import dns
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):
"""Tests for acme.dns_resolver.txt_records_for_name."""
@classmethod
def _call(cls, *args, **kwargs):
from acme.dns_resolver import txt_records_for_name
return txt_records_for_name(*args, **kwargs)
@test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT),
"optional dependency dnspython is not available")
class TxtRecordsForNameWithDnsTest(TxtRecordsForNameTest):
"""Tests for acme.dns_resolver.txt_records_for_name with dns."""
@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'], self._call('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'], self._call('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([], self._call('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([], self._call('name'))
class TxtRecordsForNameWithoutDnsTest(TxtRecordsForNameTest):
"""Tests for acme.dns_resolver.txt_records_for_name without dns."""
def setUp(self):
from acme import dns_resolver
dns_resolver.DNS_AVAILABLE = False
def tearDown(self):
from acme import dns_resolver
reload_module(dns_resolver)
def test_exception_raised(self):
self.assertRaises(
errors.DependencyError, self._call, "example.org")
if __name__ == '__main__':
unittest.main() # pragma: no cover
-16
View File
@@ -11,9 +11,7 @@ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
import OpenSSL import OpenSSL
from acme import errors
from acme import jose from acme import jose
from acme import util
def vector_path(*names): def vector_path(*names):
@@ -78,20 +76,6 @@ def load_pyopenssl_private_key(*names):
return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
def requirement_available(requirement):
"""Checks if requirement can be imported.
:rtype: bool
:returns: ``True`` iff requirement can be imported
"""
try:
util.activate(requirement)
except errors.DependencyError: # pragma: no cover
return False
return True # pragma: no cover
def skip_unless(condition, reason): # pragma: no cover def skip_unless(condition, reason): # pragma: no cover
"""Skip tests unless a condition holds. """Skip tests unless a condition holds.
-18
View File
@@ -1,25 +1,7 @@
"""ACME utilities.""" """ACME utilities."""
import pkg_resources
import six import six
from acme import errors
def map_keys(dikt, func): def map_keys(dikt, func):
"""Map dictionary keys.""" """Map dictionary keys."""
return dict((func(key), value) for key, value in six.iteritems(dikt)) return dict((func(key), value) for key, value in six.iteritems(dikt))
def activate(requirement):
"""Make requirement importable.
:param str requirement: the distribution and version to activate
:raises acme.errors.DependencyError: if cannot activate requirement
"""
try:
for distro in pkg_resources.require(requirement): # pylint: disable=not-callable
distro.activate()
except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict):
raise errors.DependencyError('{0} is unavailable'.format(requirement))
-18
View File
@@ -1,8 +1,6 @@
"""Tests for acme.util.""" """Tests for acme.util."""
import unittest import unittest
from acme import errors
class MapKeysTest(unittest.TestCase): class MapKeysTest(unittest.TestCase):
"""Tests for acme.util.map_keys.""" """Tests for acme.util.map_keys."""
@@ -14,21 +12,5 @@ class MapKeysTest(unittest.TestCase):
self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1)) self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1))
class ActivateTest(unittest.TestCase):
"""Tests for acme.util.activate."""
@classmethod
def _call(cls, *args, **kwargs):
from acme.util import activate
return activate(*args, **kwargs)
def test_failure(self):
self.assertRaises(errors.DependencyError, self._call, 'acme>99.0.0')
def test_success(self):
self._call('acme')
import acme as unused_acme
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() # pragma: no cover unittest.main() # pragma: no cover
-6
View File
@@ -33,11 +33,6 @@ 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',
'tox', 'tox',
@@ -77,7 +72,6 @@ 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,
}, },
+49 -64
View File
@@ -18,7 +18,6 @@ from certbot import errors
from certbot import interfaces from certbot import interfaces
from certbot.plugins import common from certbot.plugins import common
from certbot.plugins import util
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -208,74 +207,38 @@ class Authenticator(common.Plugin):
# pylint: disable=unused-argument,missing-docstring # pylint: disable=unused-argument,missing-docstring
return self.supported_challenges return self.supported_challenges
def _verify_ports_are_available(self, achalls):
"""Confirm the ports are available to solve all achalls.
:param list achalls: list of
:class:`~certbot.achallenges.AnnotatedChallenge`
:raises .errors.MisconfigurationError: if required port is
unavailable
"""
ports = []
if any(isinstance(ac.chall, challenges.HTTP01) for ac in achalls):
ports.append(self.config.http01_port)
if any(isinstance(ac.chall, challenges.TLSSNI01) for ac in achalls):
ports.append(self.config.tls_sni_01_port)
renewer = (self.config.verb == "renew")
if any(util.already_listening(port, renewer) for port in ports):
raise errors.MisconfigurationError(
"At least one of the required ports is already taken.")
def perform(self, achalls): # pylint: disable=missing-docstring def perform(self, achalls): # pylint: disable=missing-docstring
self._verify_ports_are_available(achalls) return [self._try_perform_single(achall) for achall in achalls]
try: def _try_perform_single(self, achall):
return self.perform2(achalls) while True:
except errors.StandaloneBindError as error: try:
display = zope.component.getUtility(interfaces.IDisplay) return self._perform_single(achall)
except errors.StandaloneBindError as error:
_handle_perform_error(error)
if error.socket_error.errno == socket.errno.EACCES: def _perform_single(self, achall):
display.notification( if isinstance(achall.chall, challenges.HTTP01):
"Could not bind TCP port {0} because you don't have " server, response = self._perform_http_01(achall)
"the appropriate permissions (for example, you " else: # tls-sni-01
"aren't running this program as " server, response = self._perform_tls_sni_01(achall)
"root).".format(error.port), force_interactive=True) self.served[server].add(achall)
elif error.socket_error.errno == socket.errno.EADDRINUSE: return response
display.notification(
"Could not bind TCP port {0} because it is already in "
"use by another process on this system (such as a web "
"server). Please stop the program in question and then "
"try again.".format(error.port), force_interactive=True)
else:
raise # XXX: How to handle unknown errors in binding?
def perform2(self, achalls): def _perform_http_01(self, achall):
"""Perform achallenges without IDisplay interaction.""" server = self.servers.run(self.config.http01_port, challenges.HTTP01)
responses = [] response, validation = achall.response_and_validation()
resource = acme_standalone.HTTP01RequestHandler.HTTP01Resource(
chall=achall.chall, response=response, validation=validation)
self.http_01_resources.add(resource)
return server, response
for achall in achalls: def _perform_tls_sni_01(self, achall):
if isinstance(achall.chall, challenges.HTTP01): port = self.config.tls_sni_01_port
server = self.servers.run( server = self.servers.run(port, challenges.TLSSNI01)
self.config.http01_port, challenges.HTTP01) response, (cert, _) = achall.response_and_validation(cert_key=self.key)
response, validation = achall.response_and_validation() self.certs[response.z_domain] = (self.key, cert)
self.http_01_resources.add( return server, response
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
chall=achall.chall, response=response,
validation=validation))
else: # tls-sni-01
server = self.servers.run(
self.config.tls_sni_01_port, challenges.TLSSNI01)
response, (cert, _) = achall.response_and_validation(
cert_key=self.key)
self.certs[response.z_domain] = (self.key, cert)
self.served[server].add(achall)
responses.append(response)
return responses
def cleanup(self, achalls): # pylint: disable=missing-docstring def cleanup(self, achalls): # pylint: disable=missing-docstring
# reduce self.served and close servers if none challenges are served # reduce self.served and close servers if none challenges are served
@@ -286,3 +249,25 @@ class Authenticator(common.Plugin):
for port, server in six.iteritems(self.servers.running()): for port, server in six.iteritems(self.servers.running()):
if not self.served[server]: if not self.served[server]:
self.servers.stop(port) self.servers.stop(port)
def _handle_perform_error(error):
if error.socket_error.errno == socket.errno.EACCES:
raise errors.PluginError(
"Could not bind TCP port {0} because you don't have "
"the appropriate permissions (for example, you "
"aren't running this program as "
"root).".format(error.port))
elif error.socket_error.errno == socket.errno.EADDRINUSE:
display = zope.component.getUtility(interfaces.IDisplay)
msg = (
"Could not bind TCP port {0} because it is already in "
"use by another process on this system (such as a web "
"server). Please stop the program in question and "
"then try again.".format(error.port))
should_retry = display.yesno(msg, "Retry",
"Cancel", default=False)
if not should_retry:
raise errors.PluginError(msg)
else:
raise
+48 -79
View File
@@ -8,11 +8,9 @@ import six
from acme import challenges from acme import challenges
from acme import jose from acme import jose
from acme import standalone as acme_standalone
from certbot import achallenges from certbot import achallenges
from certbot import errors from certbot import errors
from certbot import interfaces
from certbot.tests import acme_util from certbot.tests import acme_util
from certbot.tests import util as test_util from certbot.tests import util as test_util
@@ -114,6 +112,7 @@ def get_open_port():
open_socket.close() open_socket.close()
return port return port
class AuthenticatorTest(unittest.TestCase): class AuthenticatorTest(unittest.TestCase):
"""Tests for certbot.plugins.standalone.Authenticator.""" """Tests for certbot.plugins.standalone.Authenticator."""
@@ -124,6 +123,7 @@ class AuthenticatorTest(unittest.TestCase):
tls_sni_01_port=get_open_port(), http01_port=get_open_port(), tls_sni_01_port=get_open_port(), http01_port=get_open_port(),
standalone_supported_challenges="tls-sni-01,http-01") standalone_supported_challenges="tls-sni-01,http-01")
self.auth = Authenticator(self.config, name="standalone") self.auth = Authenticator(self.config, name="standalone")
self.auth.servers = mock.MagicMock()
def test_supported_challenges(self): def test_supported_challenges(self):
self.assertEqual(self.auth.supported_challenges, self.assertEqual(self.auth.supported_challenges,
@@ -146,6 +146,52 @@ class AuthenticatorTest(unittest.TestCase):
self.assertEqual(self.auth.get_chall_pref(domain=None), self.assertEqual(self.auth.get_chall_pref(domain=None),
[challenges.TLSSNI01]) [challenges.TLSSNI01])
def test_perform(self):
achalls = self._get_achalls()
response = self.auth.perform(achalls)
expected = [achall.response(achall.account_key) for achall in achalls]
self.assertEqual(response, expected)
@test_util.patch_get_utility()
def test_perform_eaddrinuse_retry(self, mock_get_utility):
errno = socket.errno.EADDRINUSE
error = errors.StandaloneBindError(mock.MagicMock(errno=errno), -1)
self.auth.servers.run.side_effect = [error] + 2 * [mock.MagicMock()]
mock_yesno = mock_get_utility.return_value.yesno
mock_yesno.return_value = True
self.test_perform()
self._assert_correct_yesno_call(mock_yesno)
@test_util.patch_get_utility()
def test_perform_eaddrinuse_no_retry(self, mock_get_utility):
mock_yesno = mock_get_utility.return_value.yesno
mock_yesno.return_value = False
errno = socket.errno.EADDRINUSE
self.assertRaises(errors.PluginError, self._fail_perform, errno)
self._assert_correct_yesno_call(mock_yesno)
def _assert_correct_yesno_call(self, mock_yesno):
yesno_args, yesno_kwargs = mock_yesno.call_args
self.assertTrue("in use" in yesno_args[0])
self.assertFalse(yesno_kwargs.get("default", True))
def test_perform_eacces(self):
errno = socket.errno.EACCES
self.assertRaises(errors.PluginError, self._fail_perform, errno)
def test_perform_unexpected_socket_error(self):
errno = socket.errno.ENOTCONN
self.assertRaises(
errors.StandaloneBindError, self._fail_perform, errno)
def _fail_perform(self, errno):
error = errors.StandaloneBindError(mock.MagicMock(errno=errno), -1)
self.auth.servers.run.side_effect = error
self.auth.perform(self._get_achalls())
@classmethod @classmethod
def _get_achalls(cls): def _get_achalls(cls):
domain = b'localhost' domain = b'localhost'
@@ -157,84 +203,7 @@ class AuthenticatorTest(unittest.TestCase):
return [http_01, tls_sni_01] return [http_01, tls_sni_01]
@mock.patch("certbot.plugins.standalone.util")
def test_perform_already_listening(self, mock_util):
http_01, tls_sni_01 = self._get_achalls()
for achall, port in ((http_01, self.config.http01_port,),
(tls_sni_01, self.config.tls_sni_01_port)):
mock_util.already_listening.return_value = True
self.assertRaises(
errors.MisconfigurationError, self.auth.perform, [achall])
mock_util.already_listening.assert_called_once_with(port, False)
mock_util.already_listening.reset_mock()
@test_util.patch_get_utility()
def test_perform(self, unused_mock_get_utility):
achalls = self._get_achalls()
self.auth.perform2 = mock.Mock(return_value=mock.sentinel.responses)
self.assertEqual(mock.sentinel.responses, self.auth.perform(achalls))
self.auth.perform2.assert_called_once_with(achalls)
@test_util.patch_get_utility()
def _test_perform_bind_errors(self, errno, achalls, mock_get_utility):
port = get_open_port()
def _perform2(unused_achalls):
raise errors.StandaloneBindError(mock.Mock(errno=errno), port)
self.auth.perform2 = mock.MagicMock(side_effect=_perform2)
self.auth.perform(achalls)
mock_get_utility.assert_called_once_with(interfaces.IDisplay)
notification = mock_get_utility.return_value.notification
self.assertEqual(1, notification.call_count)
self.assertTrue(str(port) in notification.call_args[0][0])
def test_perform_eacces(self):
# pylint: disable=no-value-for-parameter
self._test_perform_bind_errors(socket.errno.EACCES, [])
def test_perform_eaddrinuse(self):
# pylint: disable=no-value-for-parameter
self._test_perform_bind_errors(socket.errno.EADDRINUSE, [])
def test_perfom_unknown_bind_error(self):
self.assertRaises(
errors.StandaloneBindError, self._test_perform_bind_errors,
socket.errno.ENOTCONN, [])
def test_perform2(self):
http_01, tls_sni_01 = self._get_achalls()
self.auth.servers = mock.MagicMock()
def _run(port, tls): # pylint: disable=unused-argument
return "server{0}".format(port)
self.auth.servers.run.side_effect = _run
responses = self.auth.perform2([http_01, tls_sni_01])
self.assertTrue(isinstance(responses, list))
self.assertEqual(2, len(responses))
self.assertTrue(isinstance(responses[0], challenges.HTTP01Response))
self.assertTrue(isinstance(responses[1], challenges.TLSSNI01Response))
self.assertEqual(self.auth.servers.run.mock_calls, [
mock.call(self.config.http01_port, challenges.HTTP01),
mock.call(self.config.tls_sni_01_port, challenges.TLSSNI01),
])
self.assertEqual(self.auth.served, {
"server" + str(self.config.tls_sni_01_port): set([tls_sni_01]),
"server" + str(self.config.http01_port): set([http_01]),
})
self.assertEqual(1, len(self.auth.http_01_resources))
self.assertEqual(1, len(self.auth.certs))
self.assertEqual(list(self.auth.http_01_resources), [
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
acme_util.HTTP01, responses[0], mock.ANY)])
def test_cleanup(self): def test_cleanup(self):
self.auth.servers = mock.Mock()
self.auth.servers.running.return_value = { self.auth.servers.running.return_value = {
1: "server1", 1: "server1",
2: "server2", 2: "server2",
-125
View File
@@ -1,34 +1,11 @@
"""Plugin utilities.""" """Plugin utilities."""
import logging import logging
import os import os
import socket
import zope.component
from acme import errors as acme_errors
from acme import util as acme_util
from certbot import interfaces
from certbot import util from certbot import util
PSUTIL_REQUIREMENT = "psutil>=2.2.1"
try:
acme_util.activate(PSUTIL_REQUIREMENT)
import psutil # pragma: no cover
USE_PSUTIL = True
except acme_errors.DependencyError: # pragma: no cover
USE_PSUTIL = False
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
RENEWER_EXTRA_MSG = (
" For automated renewal, you may want to use a script that stops"
" and starts your webserver. You can find an example at"
" https://certbot.eff.org/docs/using.html#renewal ."
" Alternatively you can use the webroot plugin to renew without"
" needing to stop and start your webserver.")
def path_surgery(cmd): def path_surgery(cmd):
"""Attempt to perform PATH surgery to find cmd """Attempt to perform PATH surgery to find cmd
@@ -59,105 +36,3 @@ def path_surgery(cmd):
logger.warning("Failed to find %s in%s PATH: %s", cmd, logger.warning("Failed to find %s in%s PATH: %s", cmd,
expanded, path) expanded, path)
return False return False
def already_listening(port, renewer=False):
"""Check if a process is already listening on the port.
If so, also tell the user via a display notification.
.. warning::
On some operating systems, this function can only usefully be
run as root.
:param int port: The TCP port in question.
:returns: True or False.
"""
if USE_PSUTIL:
return already_listening_psutil(port, renewer=renewer)
else:
logger.debug("Psutil not found, using simple socket check.")
return already_listening_socket(port, renewer=renewer)
def already_listening_socket(port, renewer=False):
"""Simple socket based check to find out if port is already in use
:param int port: The TCP port in question.
:returns: True or False
"""
try:
testsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
testsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
testsocket.bind(("", port))
except socket.error:
display = zope.component.getUtility(interfaces.IDisplay)
extra = ""
if renewer:
extra = RENEWER_EXTRA_MSG
display.notification(
"Port {0} is already in use by another process. This will "
"prevent us from binding to that port. Please stop the "
"process that is populating the port in question and try "
"again. {1}".format(port, extra), force_interactive=True)
return True
finally:
testsocket.close()
except socket.error:
pass
return False
def already_listening_psutil(port, renewer=False):
"""Psutil variant of the open port check
:param int port: The TCP port in question.
:returns: True or False.
"""
try:
net_connections = psutil.net_connections()
except psutil.AccessDenied as error:
logger.info("Access denied when trying to list network "
"connections: %s. Are you root?", error)
# this function is just a pre-check that often causes false
# positives and problems in testing (c.f. #680 on Mac, #255
# generally); we will fail later in bind() anyway
return False
listeners = [conn.pid for conn in net_connections
if conn.status == 'LISTEN' and
conn.type == socket.SOCK_STREAM and
conn.laddr[1] == port]
try:
if listeners and listeners[0] is not None:
# conn.pid may be None if the current process doesn't have
# permission to identify the listening process! Additionally,
# listeners may have more than one element if separate
# sockets have bound the same port on separate interfaces.
# We currently only have UI to notify the user about one
# of them at a time.
pid = listeners[0]
name = psutil.Process(pid).name()
display = zope.component.getUtility(interfaces.IDisplay)
extra = ""
if renewer:
extra = RENEWER_EXTRA_MSG
display.notification(
"The program {0} (process ID {1}) is already listening "
"on TCP port {2}. This will prevent us from binding to "
"that port. Please stop the {0} program temporarily "
"and then try again.{3}".format(name, pid, port, extra),
force_interactive=True)
return True
except (psutil.NoSuchProcess, psutil.AccessDenied):
# Perhaps the result of a race where the process could have
# exited or relinquished the port (NoSuchProcess), or the result
# of an OS policy where we're not allowed to look up the process
# name (AccessDenied).
pass
return False
-139
View File
@@ -1,13 +1,9 @@
"""Tests for certbot.plugins.util.""" """Tests for certbot.plugins.util."""
import os import os
import socket
import unittest import unittest
import mock import mock
from certbot.plugins.util import PSUTIL_REQUIREMENT
from certbot.tests import util as test_util
class PathSurgeryTest(unittest.TestCase): class PathSurgeryTest(unittest.TestCase):
"""Tests for certbot.plugins.path_surgery.""" """Tests for certbot.plugins.path_surgery."""
@@ -34,140 +30,5 @@ class PathSurgeryTest(unittest.TestCase):
self.assertTrue("/tmp" in os.environ["PATH"]) self.assertTrue("/tmp" in os.environ["PATH"])
class AlreadyListeningTest(unittest.TestCase):
"""Tests for certbot.plugins.already_listening."""
@classmethod
def _call(cls, *args, **kwargs):
from certbot.plugins.util import already_listening
return already_listening(*args, **kwargs)
class AlreadyListeningTestNoPsutil(AlreadyListeningTest):
"""Tests for certbot.plugins.already_listening when
psutil is not available"""
@classmethod
def _call(cls, *args, **kwargs):
with mock.patch("certbot.plugins.util.USE_PSUTIL", False):
return super(
AlreadyListeningTestNoPsutil, cls)._call(*args, **kwargs)
@test_util.patch_get_utility()
def test_ports_available(self, mock_getutil):
# Ensure we don't get error
with mock.patch("socket.socket.bind"):
self.assertFalse(self._call(80))
self.assertFalse(self._call(80, True))
self.assertEqual(mock_getutil.call_count, 0)
@test_util.patch_get_utility()
def test_ports_blocked(self, mock_getutil):
with mock.patch("certbot.plugins.util.socket.socket.bind") as mock_bind:
mock_bind.side_effect = socket.error
self.assertTrue(self._call(80))
self.assertTrue(self._call(80, True))
with mock.patch("certbot.plugins.util.socket.socket") as mock_socket:
mock_socket.side_effect = socket.error
self.assertFalse(self._call(80))
self.assertEqual(mock_getutil.call_count, 2)
@test_util.skip_unless(test_util.requirement_available(PSUTIL_REQUIREMENT),
"optional dependency psutil is not available")
class AlreadyListeningTestPsutil(AlreadyListeningTest):
"""Tests for certbot.plugins.already_listening."""
@mock.patch("certbot.plugins.util.psutil.net_connections")
@mock.patch("certbot.plugins.util.psutil.Process")
@test_util.patch_get_utility()
def test_race_condition(self, mock_get_utility, mock_process, mock_net):
# This tests a race condition, or permission problem, or OS
# incompatibility in which, for some reason, no process name can be
# found to match the identified listening PID.
import psutil
from psutil._common import sconn
conns = [
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
raddr=(), status="LISTEN", pid=None),
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
raddr=(), status="LISTEN", pid=4416)]
mock_net.return_value = conns
mock_process.side_effect = psutil.NoSuchProcess("No such PID")
# We simulate being unable to find the process name of PID 4416,
# which results in returning False.
self.assertFalse(self._call(17))
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
mock_process.assert_called_once_with(4416)
@mock.patch("certbot.plugins.util.psutil.net_connections")
@mock.patch("certbot.plugins.util.psutil.Process")
@test_util.patch_get_utility()
def test_not_listening(self, mock_get_utility, mock_process, mock_net):
from psutil._common import sconn
conns = [
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
raddr=(), status="LISTEN", pid=None),
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
raddr=("::1", 111), status="CLOSE_WAIT", pid=None)]
mock_net.return_value = conns
mock_process.name.return_value = "inetd"
self.assertFalse(self._call(17))
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
self.assertEqual(mock_process.call_count, 0)
@mock.patch("certbot.plugins.util.psutil.net_connections")
@mock.patch("certbot.plugins.util.psutil.Process")
@test_util.patch_get_utility()
def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net):
from psutil._common import sconn
conns = [
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
raddr=(), status="LISTEN", pid=None),
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
raddr=(), status="LISTEN", pid=4416)]
mock_net.return_value = conns
mock_process.name.return_value = "inetd"
result = self._call(17, True)
self.assertTrue(result)
self.assertEqual(mock_get_utility.call_count, 1)
mock_process.assert_called_once_with(4416)
@mock.patch("certbot.plugins.util.psutil.net_connections")
@mock.patch("certbot.plugins.util.psutil.Process")
@test_util.patch_get_utility()
def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net):
from psutil._common import sconn
conns = [
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
raddr=(), status="LISTEN", pid=None),
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
sconn(fd=3, family=10, type=1, laddr=("::", 12345), raddr=(),
status="LISTEN", pid=4420),
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
raddr=(), status="LISTEN", pid=4416)]
mock_net.return_value = conns
mock_process.name.return_value = "inetd"
result = self._call(12345)
self.assertTrue(result)
self.assertEqual(mock_get_utility.call_count, 1)
mock_process.assert_called_once_with(4420)
@mock.patch("certbot.plugins.util.psutil.net_connections")
def test_access_denied_exception(self, mock_net):
import psutil
mock_net.side_effect = psutil.AccessDenied("")
self.assertFalse(self._call(12345))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() # pragma: no cover unittest.main() # pragma: no cover
-16
View File
@@ -13,9 +13,7 @@ from cryptography.hazmat.primitives import serialization
import mock import mock
import OpenSSL import OpenSSL
from acme import errors
from acme import jose from acme import jose
from acme import util
from certbot import constants from certbot import constants
from certbot import interfaces from certbot import interfaces
@@ -86,20 +84,6 @@ def load_pyopenssl_private_key(*names):
return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
def requirement_available(requirement):
"""Checks if requirement can be imported.
:rtype: bool
:returns: ``True`` iff requirement can be imported
"""
try:
util.activate(requirement)
except errors.DependencyError: # pragma: no cover
return False
return True # pragma: no cover
def skip_unless(condition, reason): # pragma: no cover def skip_unless(condition, reason): # pragma: no cover
"""Skip tests unless a condition holds. """Skip tests unless a condition holds.
-1
View File
@@ -67,7 +67,6 @@ dev_extras = [
'astroid==1.3.5', 'astroid==1.3.5',
'coverage', 'coverage',
'nose', 'nose',
'psutil>=2.2.1', # for tests, optional
'pylint==1.4.2', # upstream #248 'pylint==1.4.2', # upstream #248
'tox', 'tox',
'twine', 'twine',
+8 -10
View File
@@ -13,7 +13,7 @@ envlist = modification,py{26,33,34,35,36},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[dns,dev] pip install -e acme[dev]
nosetests -v acme --processes=-1 nosetests -v acme --processes=-1
pip install -e .[dev] pip install -e .[dev]
nosetests -v certbot --processes=-1 --process-timeout=100 nosetests -v certbot --processes=-1 --process-timeout=100
@@ -37,35 +37,33 @@ deps =
py{26,27}-oldest: cffi<=1.7 py{26,27}-oldest: cffi<=1.7
py{26,27}-oldest: cryptography==0.8 py{26,27}-oldest: cryptography==0.8
py{26,27}-oldest: configargparse==0.10.0 py{26,27}-oldest: configargparse==0.10.0
py{26,27}-oldest: dnspython>=1.12
py{26,27}-oldest: psutil==2.1.0
py{26,27}-oldest: PyOpenSSL==0.13 py{26,27}-oldest: PyOpenSSL==0.13
py{26,27}-oldest: requests<=2.11.1 py{26,27}-oldest: requests<=2.11.1
[testenv:py33] [testenv:py33]
commands = commands =
pip install -e acme[dns,dev] pip install -e acme[dev]
nosetests -v acme --processes=-1 nosetests -v acme --processes=-1
pip install -e .[dev] pip install -e .[dev]
nosetests -v certbot --processes=-1 --process-timeout=100 nosetests -v certbot --processes=-1 --process-timeout=100
[testenv:py34] [testenv:py34]
commands = commands =
pip install -e acme[dns,dev] pip install -e acme[dev]
nosetests -v acme --processes=-1 nosetests -v acme --processes=-1
pip install -e .[dev] pip install -e .[dev]
nosetests -v certbot --processes=-1 --process-timeout=100 nosetests -v certbot --processes=-1 --process-timeout=100
[testenv:py35] [testenv:py35]
commands = commands =
pip install -e acme[dns,dev] pip install -e acme[dev]
nosetests -v acme --processes=-1 nosetests -v acme --processes=-1
pip install -e .[dev] pip install -e .[dev]
nosetests -v certbot --processes=-1 --process-timeout=100 nosetests -v certbot --processes=-1 --process-timeout=100
[testenv:py36] [testenv:py36]
commands = commands =
pip install -e acme[dns,dev] pip install -e acme[dev]
nosetests -v acme --processes=-1 nosetests -v acme --processes=-1
pip install -e .[dev] pip install -e .[dev]
nosetests -v certbot --processes=-1 --process-timeout=100 nosetests -v certbot --processes=-1 --process-timeout=100
@@ -73,12 +71,12 @@ commands =
[testenv:py27_install] [testenv:py27_install]
basepython = python2.7 basepython = python2.7
commands = commands =
pip install -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot pip install -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot
[testenv:cover] [testenv:cover]
basepython = python2.7 basepython = python2.7
commands = commands =
pip install -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot pip install -e acme[dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot
./tox.cover.sh ./tox.cover.sh
[testenv:lint] [testenv:lint]
@@ -88,7 +86,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[dns,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
pylint --reports=n --rcfile=.pylintrc acme/acme certbot certbot-apache/certbot_apache certbot-nginx/certbot_nginx certbot-compatibility-test/certbot_compatibility_test letshelp-certbot/letshelp_certbot pylint --reports=n --rcfile=.pylintrc acme/acme certbot certbot-apache/certbot_apache certbot-nginx/certbot_nginx certbot-compatibility-test/certbot_compatibility_test letshelp-certbot/letshelp_certbot
[testenv:apacheconftest] [testenv:apacheconftest]