mirror of
https://github.com/certbot/certbot.git
synced 2026-07-31 18:45:40 +02:00
start of tests for auth_handler
This commit is contained in:
@@ -10,7 +10,6 @@ class Error(jose.JSONObjectWithFields, Exception):
|
||||
https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
|
||||
|
||||
"""
|
||||
|
||||
ERROR_TYPE_NAMESPACE = 'urn:acme:error:'
|
||||
ERROR_TYPE_DESCRIPTIONS = {
|
||||
'malformed': 'The request message was malformed',
|
||||
@@ -73,6 +72,9 @@ class _Constant(jose.JSONDeSerializable):
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, type(self)) and other.name == self.name
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
class Status(_Constant):
|
||||
"""ACME "status" field."""
|
||||
@@ -133,7 +135,6 @@ class Registration(ResourceBody):
|
||||
:ivar tuple contact: Contact information following ACME spec
|
||||
|
||||
"""
|
||||
|
||||
# on new-reg key server ignores 'key' and populates it based on
|
||||
# JWS.signature.combined.jwk
|
||||
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
|
||||
@@ -217,7 +218,6 @@ class Authorization(ResourceBody):
|
||||
:ivar datetime.datetime expires:
|
||||
|
||||
"""
|
||||
|
||||
identifier = jose.Field('identifier', decoder=Identifier.from_json)
|
||||
challenges = jose.Field('challenges', omitempty=True)
|
||||
combinations = jose.Field('combinations', omitempty=True)
|
||||
|
||||
@@ -67,6 +67,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
for domain in domains:
|
||||
self.authzr[domain] = self.network.request_domain_challenges(
|
||||
domain, self.account.new_authzr_uri)
|
||||
|
||||
self._choose_challenges(domains)
|
||||
|
||||
# While there are still challenges remaining...
|
||||
@@ -77,7 +78,11 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
# Send all Responses - this modifies dv_c and cont_c
|
||||
self._respond(cont_resp, dv_resp, best_effort)
|
||||
|
||||
return self.authzr.values()
|
||||
# Just make sure all decisions are complete.
|
||||
self._verify_authzr_complete()
|
||||
# Only return valid authorizations
|
||||
return [authzr for authzr in self.authzr.values()
|
||||
if authzr.body.status == messages2.STATUS_VALID]
|
||||
|
||||
def _choose_challenges(self, domains):
|
||||
"""Retrieve necessary challenges to satisfy server."""
|
||||
@@ -88,7 +93,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
self._get_chall_pref(dom),
|
||||
self.authzr[dom].body.combinations)
|
||||
|
||||
dom_dv_c, dom_cont_c = self._challenge_factory(
|
||||
dom_cont_c, dom_dv_c = self._challenge_factory(
|
||||
dom, path)
|
||||
self.dv_c.extend(dom_dv_c)
|
||||
self.cont_c.extend(dom_cont_c)
|
||||
@@ -123,11 +128,16 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
"""
|
||||
# TODO: chall_update is a dirty hack to get around acme-spec #105
|
||||
chall_update = dict()
|
||||
self._send_responses(self.dv_c, dv_resp, chall_update)
|
||||
self._send_responses(self.cont_c, cont_resp, chall_update)
|
||||
active_achalls = []
|
||||
active_achalls.extend(
|
||||
self._send_responses(self.dv_c, dv_resp, chall_update))
|
||||
active_achalls.extend(
|
||||
self._send_responses(self.cont_c, cont_resp, chall_update))
|
||||
|
||||
# Check for updated status...
|
||||
self._poll_challenges(chall_update, best_effort)
|
||||
# This removes challenges from self.dv_c and self.cont_c
|
||||
self._cleanup_challenges(active_achalls)
|
||||
|
||||
def _send_responses(self, achalls, resps, chall_update):
|
||||
"""Send responses and make sure errors are handled.
|
||||
@@ -136,15 +146,19 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
authzr -> list of outstanding solved annotated challenges
|
||||
|
||||
"""
|
||||
active_achalls = []
|
||||
for achall, resp in itertools.izip(achalls, resps):
|
||||
# Don't send challenges for None and False authenticator responses
|
||||
if resp:
|
||||
challr = self.network.answer_challenge(achall.challb, resp)
|
||||
self.network.answer_challenge(achall.challb, resp)
|
||||
active_achalls.append(achall)
|
||||
if achall.domain in chall_update:
|
||||
chall_update[achall.domain].append(achall)
|
||||
else:
|
||||
chall_update[achall.domain] = [achall]
|
||||
|
||||
return active_achalls
|
||||
|
||||
def _poll_challenges(self, chall_update, best_effort, min_sleep=3):
|
||||
"""Wait for all challenge results to be determined."""
|
||||
dom_to_check = set(chall_update.keys())
|
||||
@@ -166,15 +180,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
else:
|
||||
# Right now... just assume a loss and carry on...
|
||||
if best_effort:
|
||||
# Add to completed list... but remove authzr
|
||||
del self.authzr[domain]
|
||||
comp_domains.add(domain)
|
||||
|
||||
else:
|
||||
raise errors.AuthorizationError(
|
||||
"Failed Authorization procedure for %s" % domain)
|
||||
|
||||
self._cleanup_challenges(comp_challs+failed_challs)
|
||||
|
||||
dom_to_check -= comp_domains
|
||||
comp_domains.clear()
|
||||
|
||||
@@ -191,7 +202,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
# challenges will be determined here...
|
||||
for achall in achalls:
|
||||
status = self._get_chall_status(self.authzr[domain], achall)
|
||||
print "Status:", status
|
||||
|
||||
# This does nothing for challenges that have yet to be decided yet.
|
||||
if status == messages2.STATUS_VALID:
|
||||
completed.append(achall)
|
||||
@@ -200,16 +211,16 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
return completed, failed
|
||||
|
||||
def _get_chall_status(self, authzr, chall):
|
||||
def _get_chall_status(self, authzr, achall):
|
||||
"""Get the status of the challenge.
|
||||
|
||||
.. warning:: This assumes only one instance of type of challenge in
|
||||
each challenge resource.
|
||||
|
||||
"""
|
||||
for authzr_chall in authzr:
|
||||
if type(authzr_chall) is type(chall):
|
||||
return chall.status
|
||||
for authzr_challb in authzr.body.challenges:
|
||||
if type(authzr_challb.chall) is type(achall.challb.chall):
|
||||
return achall.challb.status
|
||||
raise errors.AuthorizationError(
|
||||
"Target challenge not found in authorization resource")
|
||||
|
||||
@@ -219,7 +230,9 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
:param str domain: domain for which you are requesting preferences
|
||||
|
||||
"""
|
||||
chall_prefs = self.cont_auth.get_chall_pref(domain)
|
||||
# Make sure to make a copy...
|
||||
chall_prefs = []
|
||||
chall_prefs.extend(self.cont_auth.get_chall_pref(domain))
|
||||
chall_prefs.extend(self.dv_auth.get_chall_pref(domain))
|
||||
return chall_prefs
|
||||
|
||||
@@ -249,6 +262,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
for achall in cont_c:
|
||||
self.cont_c.remove(achall)
|
||||
|
||||
def _verify_authzr_complete(self):
|
||||
for authzr in self.authzr.values():
|
||||
if (authzr.body.status != messages2.STATUS_VALID and
|
||||
authzr.body.status != messages2.STATUS_INVALID):
|
||||
raise errors.AuthorizationError("Incomplete authorizations")
|
||||
|
||||
def _challenge_factory(self, domain, path):
|
||||
"""Construct Namedtuple Challenges
|
||||
|
||||
@@ -266,8 +285,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
recognized
|
||||
|
||||
"""
|
||||
dv_chall = set()
|
||||
cont_chall = set()
|
||||
dv_chall = []
|
||||
cont_chall = []
|
||||
|
||||
for index in path:
|
||||
challb = self.authzr[domain].body.challenges[index]
|
||||
@@ -303,11 +322,11 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
chall.typ)
|
||||
|
||||
if isinstance(chall, challenges.ContinuityChallenge):
|
||||
cont_chall.add(achall)
|
||||
cont_chall.append(achall)
|
||||
elif isinstance(chall, challenges.DVChallenge):
|
||||
dv_chall.add(achall)
|
||||
dv_chall.append(achall)
|
||||
|
||||
return dv_chall, cont_chall
|
||||
return cont_chall, dv_chall
|
||||
|
||||
|
||||
def gen_challenge_path(challbs, preferences, combinations):
|
||||
|
||||
@@ -78,6 +78,7 @@ def chall_to_challb(chall, status):
|
||||
|
||||
"""
|
||||
kwargs = {
|
||||
"chall": chall,
|
||||
"uri": chall.typ+"_uri",
|
||||
"status": messages2.Status(status),
|
||||
}
|
||||
@@ -88,6 +89,24 @@ def chall_to_challb(chall, status):
|
||||
return messages2.ChallengeBody(**kwargs)
|
||||
|
||||
|
||||
# Pending ChallengeBody objects
|
||||
DVSNI_P = chall_to_challb(DVSNI, "pending")
|
||||
SIMPLE_HTTPS_P = chall_to_challb(SIMPLE_HTTPS, "pending")
|
||||
DNS_P = chall_to_challb(DNS, "pending")
|
||||
RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, "pending")
|
||||
RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, "pending")
|
||||
POP_P = chall_to_challb(POP, "pending")
|
||||
|
||||
CHALLENGES_P = [SIMPLE_HTTPS_P, DVSNI_P, DNS_P,
|
||||
RECOVERY_CONTACT_P, RECOVERY_TOKEN_P, POP_P]
|
||||
DV_CHALLENGES_P = [challb for challb in CHALLENGES_P
|
||||
if isinstance(challb.chall, challenges.DVChallenge)]
|
||||
CONT_CHALLENGES_P = [
|
||||
challb for challb in CHALLENGES_P
|
||||
if isinstance(challb.chall, challenges.ContinuityChallenge)
|
||||
]
|
||||
|
||||
|
||||
def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
||||
"""Generate an authorization resource.
|
||||
|
||||
@@ -103,7 +122,7 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
||||
]
|
||||
authz_kwargs = {
|
||||
"identifier": messages2.Identifier(
|
||||
type=messages2.IDENTIFIER_FQDN, value=domain),
|
||||
typ=messages2.IDENTIFIER_FQDN, value=domain),
|
||||
"challenges": challbs,
|
||||
}
|
||||
if combos:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Tests for letsencrypt.client.auth_handler."""
|
||||
import functools
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
@@ -11,6 +12,7 @@ from letsencrypt.client import account
|
||||
from letsencrypt.client import achallenges
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import le_util
|
||||
from letsencrypt.client import network2
|
||||
|
||||
from letsencrypt.client.tests import acme_util
|
||||
|
||||
@@ -25,8 +27,50 @@ TRANSLATE = {
|
||||
}
|
||||
|
||||
|
||||
class SolveChallengesTest(unittest.TestCase):
|
||||
"""verify_identities test."""
|
||||
class ChallengeFactoryTest(unittest.TestCase):
|
||||
# pylint: disable=protected-access
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.client.auth_handler import AuthHandler
|
||||
|
||||
# Account is mocked...
|
||||
self.handler = AuthHandler(
|
||||
None, None, None, mock.Mock(key="mock_key"))
|
||||
|
||||
self.dom = "test"
|
||||
self.handler.authzr[self.dom] = acme_util.gen_authzr(
|
||||
"pending", self.dom, acme_util.CHALLENGES, ["pending"]*6, False)
|
||||
|
||||
def test_all(self):
|
||||
cont_c, dv_c = self.handler._challenge_factory(self.dom, range(0, 6))
|
||||
|
||||
self.assertEqual(
|
||||
[achall.chall for achall in cont_c], acme_util.CONT_CHALLENGES)
|
||||
self.assertEqual(
|
||||
[achall.chall for achall in dv_c], acme_util.DV_CHALLENGES)
|
||||
|
||||
def test_one_dv_one_cont(self):
|
||||
cont_c, dv_c = self.handler._challenge_factory(self.dom, [1, 4])
|
||||
|
||||
self.assertEqual(
|
||||
[achall.chall for achall in cont_c], [acme_util.RECOVERY_TOKEN])
|
||||
self.assertEqual([achall.chall for achall in dv_c], [acme_util.DVSNI])
|
||||
|
||||
def test_unrecognized(self):
|
||||
self.handler.authzr["failure.com"] = acme_util.gen_authzr(
|
||||
"pending", "failure.com",
|
||||
[mock.Mock(chall="chall", typ="unrecognized")], ["pending"])
|
||||
|
||||
self.assertRaises(errors.LetsEncryptClientError,
|
||||
self.handler._challenge_factory, "failure.com", [0])
|
||||
|
||||
|
||||
class GetAuthorizationsTest(unittest.TestCase):
|
||||
"""get_authorizations test.
|
||||
|
||||
This tests everything except for all functions under _poll_challenges.
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.client.auth_handler import AuthHandler
|
||||
@@ -41,294 +85,68 @@ class SolveChallengesTest(unittest.TestCase):
|
||||
self.mock_cont_auth.perform.side_effect = gen_auth_resp
|
||||
self.mock_dv_auth.perform.side_effect = gen_auth_resp
|
||||
|
||||
self.account = account.Account(None, le_util.Key("filepath", "pem"))
|
||||
self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM"))
|
||||
self.mock_net = mock.MagicMock(spec=network2.Network)
|
||||
|
||||
self.handler = AuthHandler(
|
||||
self.mock_dv_auth, self.mock_cont_auth, None, account)
|
||||
self.mock_dv_auth, self.mock_cont_auth,
|
||||
self.mock_net, self.mock_account)
|
||||
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
def tearDown(self):
|
||||
logging.disable(logging.NOTSET)
|
||||
|
||||
def test_name1_dvsni1(self):
|
||||
# pylint: disable=protected-access
|
||||
dom = "0"
|
||||
# Note:
|
||||
self.handler.dv_c = []
|
||||
cont_resp, dv_resp = self.handler._solve_challenges()
|
||||
@mock.patch("letsencrypt.client.auth_handler.AuthHandler._poll_challenges")
|
||||
def test_name1_dvsni1(self, mock_poll):
|
||||
self.mock_net.request_domain_challenges.side_effect = functools.partial(
|
||||
gen_dom_authzr, challs=acme_util.DV_CHALLENGES)
|
||||
|
||||
self.assertEqual(len(self.handler.responses[dom]), 1)
|
||||
mock_poll.side_effect = self._validate_all
|
||||
|
||||
self.assertEqual("DVSNI0", self.handler.responses[dom][0])
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 0)
|
||||
authzr = self.handler.get_authorizations(["0"])
|
||||
|
||||
def test_name1_rectok1(self):
|
||||
dom = "0"
|
||||
msg = messages.Challenge(
|
||||
session_id=dom, nonce="nonce0", combinations=[],
|
||||
challenges=[acme_util.RECOVERY_TOKEN])
|
||||
self.handler.add_chall_msg(dom, msg, "dummy_key")
|
||||
self.assertEqual(self.mock_net.answer_challenge.call_count, 1)
|
||||
|
||||
self.handler._satisfy_challenges() # pylint: disable=protected-access
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 1)
|
||||
self.assertEqual(len(self.handler.responses[dom]), 1)
|
||||
|
||||
# Test if statement for dv_auth perform
|
||||
self.assertEqual(self.mock_cont_auth.perform.call_count, 1)
|
||||
self.assertEqual(self.mock_dv_auth.perform.call_count, 0)
|
||||
|
||||
self.assertEqual("RecoveryToken0", self.handler.responses[dom][0])
|
||||
# Assert 1 domain
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 1)
|
||||
# Assert 1 auth challenge, 0 dv
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 0)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 1)
|
||||
|
||||
def test_name5_dvsni5(self):
|
||||
for i in xrange(5):
|
||||
self.handler.add_chall_msg(
|
||||
str(i),
|
||||
messages.Challenge(session_id=str(i), nonce="nonce%d" % i,
|
||||
challenges=[acme_util.DVSNI],
|
||||
combinations=[]),
|
||||
"dummy_key")
|
||||
|
||||
self.handler._satisfy_challenges() # pylint: disable=protected-access
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 5)
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.cont_c), 5)
|
||||
# Each message contains 1 auth, 0 client
|
||||
|
||||
# Test proper call count for methods
|
||||
self.assertEqual(self.mock_cont_auth.perform.call_count, 0)
|
||||
self.assertEqual(self.mock_dv_auth.perform.call_count, 1)
|
||||
|
||||
for i in xrange(5):
|
||||
dom = str(i)
|
||||
self.assertEqual(len(self.handler.responses[dom]), 1)
|
||||
self.assertEqual(self.handler.responses[dom][0], "DVSNI%d" % i)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 0)
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.DVSNI))
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
def test_name1_auth(self, mock_chall_path):
|
||||
dom = "0"
|
||||
|
||||
self.handler.add_chall_msg(
|
||||
dom,
|
||||
messages.Challenge(
|
||||
session_id="0", nonce="nonce0",
|
||||
challenges=acme_util.DV_CHALLENGES,
|
||||
combinations=acme_util.gen_combos(acme_util.DV_CHALLENGES)),
|
||||
"dummy_key")
|
||||
|
||||
path = gen_path([acme_util.SIMPLE_HTTPS], acme_util.DV_CHALLENGES)
|
||||
mock_chall_path.return_value = path
|
||||
self.handler._satisfy_challenges() # pylint: disable=protected-access
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 1)
|
||||
self.assertEqual(len(self.handler.responses[dom]),
|
||||
len(acme_util.DV_CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 1)
|
||||
|
||||
# Test if statement for cont_auth perform
|
||||
self.assertEqual(self.mock_cont_auth.perform.call_count, 0)
|
||||
self.assertEqual(self.mock_dv_auth.perform.call_count, 1)
|
||||
self.assertEqual(mock_poll.call_count, 1)
|
||||
chall_update = mock_poll.call_args[0][0]
|
||||
self.assertEqual(chall_update.keys(), ["0"])
|
||||
self.assertEqual(len(chall_update.values()), 1)
|
||||
|
||||
self.assertEqual(self.mock_dv_auth.cleanup.call_count, 1)
|
||||
self.assertEqual(self.mock_cont_auth.cleanup.call_count, 0)
|
||||
# Test if list first element is DVSNI, use typ because it is an achall
|
||||
self.assertEqual(
|
||||
self.handler.responses[dom],
|
||||
self._get_exp_response(dom, path, acme_util.DV_CHALLENGES))
|
||||
self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "dvsni")
|
||||
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 0)
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.SimpleHTTPS))
|
||||
self.assertEqual(len(authzr), 1)
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
def test_name1_all(self, mock_chall_path):
|
||||
dom = "0"
|
||||
@mock.patch("letsencrypt.client.auth_handler.AuthHandler._poll_challenges")
|
||||
def test_name3_dvsni3_rectok_3(self, mock_poll):
|
||||
self.mock_net.request_domain_challenges.side_effect = functools.partial(
|
||||
gen_dom_authzr, challs=acme_util.CHALLENGES)
|
||||
|
||||
combos = acme_util.gen_combos(acme_util.CHALLENGES)
|
||||
self.handler.add_chall_msg(
|
||||
dom,
|
||||
messages.Challenge(
|
||||
session_id=dom, nonce="nonce0", challenges=acme_util.CHALLENGES,
|
||||
combinations=combos),
|
||||
"dummy_key")
|
||||
mock_poll.side_effect = self._validate_all
|
||||
|
||||
path = gen_path([acme_util.SIMPLE_HTTPS, acme_util.RECOVERY_TOKEN],
|
||||
acme_util.CHALLENGES)
|
||||
mock_chall_path.return_value = path
|
||||
authzr = self.handler.get_authorizations(["0", "1", "2"])
|
||||
|
||||
self.handler._satisfy_challenges() # pylint: disable=protected-access
|
||||
self.assertEqual(self.mock_net.answer_challenge.call_count, 6)
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 1)
|
||||
self.assertEqual(
|
||||
len(self.handler.responses[dom]), len(acme_util.CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 1)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 1)
|
||||
# Check poll call
|
||||
self.assertEqual(mock_poll.call_count, 1)
|
||||
chall_update = mock_poll.call_args[0][0]
|
||||
self.assertEqual(len(chall_update.keys()), 3)
|
||||
self.assertTrue("0" in chall_update.keys())
|
||||
self.assertEqual(len(chall_update["0"]), 2)
|
||||
self.assertTrue("1" in chall_update.keys())
|
||||
self.assertEqual(len(chall_update["1"]), 2)
|
||||
self.assertTrue("2" in chall_update.keys())
|
||||
self.assertEqual(len(chall_update["2"]), 2)
|
||||
|
||||
self.assertEqual(
|
||||
self.handler.responses[dom],
|
||||
self._get_exp_response(dom, path, acme_util.CHALLENGES))
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.SimpleHTTPS))
|
||||
self.assertTrue(isinstance(self.handler.cont_c[dom][0].achall,
|
||||
achallenges.RecoveryToken))
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
def test_name5_all(self, mock_chall_path):
|
||||
combos = acme_util.gen_combos(acme_util.CHALLENGES)
|
||||
for i in xrange(5):
|
||||
self.handler.add_chall_msg(
|
||||
str(i),
|
||||
messages.Challenge(
|
||||
session_id=str(i), nonce="nonce%d" % i,
|
||||
challenges=acme_util.CHALLENGES, combinations=combos),
|
||||
"dummy_key")
|
||||
|
||||
path = gen_path([acme_util.DVSNI, acme_util.RECOVERY_CONTACT],
|
||||
acme_util.CHALLENGES)
|
||||
mock_chall_path.return_value = path
|
||||
|
||||
self.handler._satisfy_challenges() # pylint: disable=protected-access
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 5)
|
||||
for i in xrange(5):
|
||||
self.assertEqual(
|
||||
len(self.handler.responses[str(i)]), len(acme_util.CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.cont_c), 5)
|
||||
|
||||
for i in xrange(5):
|
||||
dom = str(i)
|
||||
self.assertEqual(
|
||||
self.handler.responses[dom],
|
||||
self._get_exp_response(dom, path, acme_util.CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 1)
|
||||
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.DVSNI))
|
||||
self.assertTrue(isinstance(self.handler.cont_c[dom][0].achall,
|
||||
achallenges.RecoveryContact))
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
def test_name5_mix(self, mock_chall_path):
|
||||
paths = []
|
||||
chosen_chall = [[acme_util.DNS],
|
||||
[acme_util.DVSNI],
|
||||
[acme_util.SIMPLE_HTTPS, acme_util.POP],
|
||||
[acme_util.SIMPLE_HTTPS],
|
||||
[acme_util.DNS, acme_util.RECOVERY_TOKEN]]
|
||||
challenge_list = [acme_util.DV_CHALLENGES,
|
||||
[acme_util.DVSNI],
|
||||
acme_util.CHALLENGES,
|
||||
acme_util.DV_CHALLENGES,
|
||||
acme_util.CHALLENGES]
|
||||
|
||||
# Combos doesn't matter since I am overriding the gen_path function
|
||||
for i in xrange(5):
|
||||
dom = str(i)
|
||||
paths.append(gen_path(chosen_chall[i], challenge_list[i]))
|
||||
self.handler.add_chall_msg(
|
||||
dom,
|
||||
messages.Challenge(
|
||||
session_id=dom, nonce="nonce%d" % i,
|
||||
challenges=challenge_list[i], combinations=[]),
|
||||
"dummy_key")
|
||||
|
||||
mock_chall_path.side_effect = paths
|
||||
|
||||
self.handler._satisfy_challenges() # pylint: disable=protected-access
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 5)
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.cont_c), 5)
|
||||
|
||||
for i in xrange(5):
|
||||
dom = str(i)
|
||||
resp = self._get_exp_response(i, paths[i], challenge_list[i])
|
||||
self.assertEqual(self.handler.responses[dom], resp)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(
|
||||
len(self.handler.cont_c[dom]), len(chosen_chall[i]) - 1)
|
||||
|
||||
self.assertTrue(isinstance(
|
||||
self.handler.dv_c["0"][0].achall, achallenges.DNS))
|
||||
self.assertTrue(isinstance(
|
||||
self.handler.dv_c["1"][0].achall, achallenges.DVSNI))
|
||||
self.assertTrue(isinstance(
|
||||
self.handler.dv_c["2"][0].achall, achallenges.SimpleHTTPS))
|
||||
self.assertTrue(isinstance(
|
||||
self.handler.dv_c["3"][0].achall, achallenges.SimpleHTTPS))
|
||||
self.assertTrue(isinstance(
|
||||
self.handler.dv_c["4"][0].achall, achallenges.DNS))
|
||||
|
||||
self.assertTrue(isinstance(self.handler.cont_c["2"][0].achall,
|
||||
achallenges.ProofOfPossession))
|
||||
self.assertTrue(isinstance(
|
||||
self.handler.cont_c["4"][0].achall, achallenges.RecoveryToken))
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
def test_perform_exception_cleanup(self, mock_chall_path):
|
||||
"""3 Challenge messages... fail perform... clean up."""
|
||||
# pylint: disable=protected-access
|
||||
self.mock_dv_auth.perform.side_effect = errors.LetsEncryptDvsniError
|
||||
|
||||
combos = acme_util.gen_combos(acme_util.CHALLENGES)
|
||||
|
||||
for i in xrange(3):
|
||||
self.handler.add_chall_msg(
|
||||
str(i),
|
||||
messages2.Challenge(
|
||||
session_id=str(i), nonce="nonce%d" % i,
|
||||
challenges=acme_util.CHALLENGES, combinations=combos),
|
||||
"dummy_key")
|
||||
|
||||
mock_chall_path.side_effect = [
|
||||
gen_path([acme_util.DVSNI, acme_util.POP], acme_util.CHALLENGES),
|
||||
gen_path([acme_util.POP], acme_util.CHALLENGES),
|
||||
gen_path([acme_util.DVSNI], acme_util.CHALLENGES),
|
||||
]
|
||||
|
||||
# This may change in the future... but for now catch the error
|
||||
self.assertRaises(errors.LetsEncryptAuthHandlerError,
|
||||
self.handler._satisfy_challenges)
|
||||
|
||||
# Verify cleanup is actually run correctly
|
||||
self.assertEqual(self.mock_dv_auth.cleanup.call_count, 2)
|
||||
self.assertEqual(self.mock_cont_auth.cleanup.call_count, 2)
|
||||
|
||||
|
||||
dv_cleanup_args = self.mock_dv_auth.cleanup.call_args_list
|
||||
cont_cleanup_args = self.mock_cont_auth.cleanup.call_args_list
|
||||
|
||||
# Check DV cleanup
|
||||
for i in xrange(2):
|
||||
dv_chall_list = dv_cleanup_args[i][0][0]
|
||||
self.assertEqual(len(dv_chall_list), 1)
|
||||
self.assertTrue(
|
||||
isinstance(dv_chall_list[0], achallenges.DVSNI))
|
||||
|
||||
|
||||
# Check Auth cleanup
|
||||
for i in xrange(2):
|
||||
cont_chall_list = cont_cleanup_args[i][0][0]
|
||||
self.assertEqual(len(cont_chall_list), 1)
|
||||
self.assertTrue(
|
||||
isinstance(cont_chall_list[0], achallenges.ProofOfPossession))
|
||||
self.assertEqual(self.mock_dv_auth.cleanup.call_count, 1)
|
||||
self.assertEqual(self.mock_cont_auth.cleanup.call_count, 1)
|
||||
|
||||
self.assertEqual(len(authzr), 3)
|
||||
|
||||
def _get_exp_response(self, domain, path, challs):
|
||||
# pylint: disable=no-self-use
|
||||
@@ -338,134 +156,12 @@ class SolveChallengesTest(unittest.TestCase):
|
||||
|
||||
return exp_resp
|
||||
|
||||
|
||||
# pylint: disable=protected-access
|
||||
class GetAuthorizationsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.auth_handler import AuthHandler
|
||||
|
||||
self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator")
|
||||
self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator")
|
||||
|
||||
self.mock_sat_chall = mock.MagicMock(name="_satisfy_challenges")
|
||||
self.mock_acme_auth = mock.MagicMock(name="acme_authorization")
|
||||
|
||||
self.iteration = 0
|
||||
|
||||
self.handler = AuthHandler(
|
||||
self.mock_dv_auth, self.mock_cont_auth, None)
|
||||
|
||||
self.handler._satisfy_challenges = self.mock_sat_chall
|
||||
self.handler.acme_authorization = self.mock_acme_auth
|
||||
|
||||
def test_solved3_at_once(self):
|
||||
# Set 3 DVSNI challenges
|
||||
for i in xrange(3):
|
||||
self.handler.add_chall_msg(
|
||||
str(i),
|
||||
messages.Challenge(
|
||||
session_id=str(i), nonce="nonce%d" % i,
|
||||
challenges=[acme_util.DVSNI], combinations=[]),
|
||||
"dummy_key")
|
||||
|
||||
self.mock_sat_chall.side_effect = self._sat_solved_at_once
|
||||
self.handler.get_authorizations()
|
||||
|
||||
self.assertEqual(self.mock_sat_chall.call_count, 1)
|
||||
self.assertEqual(self.mock_acme_auth.call_count, 3)
|
||||
|
||||
exp_call_list = [mock.call("0"), mock.call("1"), mock.call("2")]
|
||||
self.assertEqual(
|
||||
self.mock_acme_auth.call_args_list, exp_call_list)
|
||||
self._test_finished()
|
||||
|
||||
def _sat_solved_at_once(self):
|
||||
for i in xrange(3):
|
||||
dom = str(i)
|
||||
self.handler.responses[dom] = ["DVSNI%d" % i]
|
||||
self.handler.paths[dom] = [0]
|
||||
# Assignment was > 80 char...
|
||||
dv_c, c_c = self.handler._challenge_factory(dom, [0])
|
||||
|
||||
self.handler.dv_c[dom], self.handler.cont_c[dom] = dv_c, c_c
|
||||
|
||||
def test_progress_failure(self):
|
||||
self.handler.add_chall_msg(
|
||||
"0",
|
||||
messages.Challenge(
|
||||
session_id="0", nonce="nonce0", challenges=acme_util.CHALLENGES,
|
||||
combinations=[]),
|
||||
"dummy_key")
|
||||
|
||||
# Don't do anything to satisfy challenges
|
||||
self.mock_sat_chall.side_effect = self._sat_failure
|
||||
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptAuthHandlerError, self.handler.get_authorizations)
|
||||
|
||||
# Check to make sure program didn't loop
|
||||
self.assertEqual(self.mock_sat_chall.call_count, 1)
|
||||
|
||||
def _sat_failure(self):
|
||||
dom = "0"
|
||||
self.handler.paths[dom] = gen_path(
|
||||
[acme_util.DNS, acme_util.RECOVERY_TOKEN],
|
||||
self.handler.msgs[dom].challenges)
|
||||
dv_c, c_c = self.handler._challenge_factory(
|
||||
dom, self.handler.paths[dom])
|
||||
self.handler.dv_c[dom], self.handler.cont_c[dom] = dv_c, c_c
|
||||
|
||||
def test_incremental_progress(self):
|
||||
for dom, challs in [("0", acme_util.CHALLENGES),
|
||||
("1", acme_util.DV_CHALLENGES)]:
|
||||
self.handler.add_chall_msg(
|
||||
dom,
|
||||
messages.Challenge(session_id=dom, nonce="nonce",
|
||||
combinations=[], challenges=challs),
|
||||
"dummy_key")
|
||||
|
||||
self.mock_sat_chall.side_effect = self._sat_incremental
|
||||
|
||||
self.handler.get_authorizations()
|
||||
|
||||
self._test_finished()
|
||||
self.assertEqual(self.mock_acme_auth.call_args_list,
|
||||
[mock.call("1"), mock.call("0")])
|
||||
|
||||
def _sat_incremental(self):
|
||||
# Exact responses don't matter, just path/response match
|
||||
if self.iteration == 0:
|
||||
# Only solve one of "0" required challs
|
||||
self.handler.responses["0"][1] = "onecomplete"
|
||||
self.handler.responses["0"][3] = None
|
||||
self.handler.responses["1"] = [None, None, "goodresp"]
|
||||
self.handler.paths["0"] = [1, 3]
|
||||
self.handler.paths["1"] = [2]
|
||||
# This is probably overkill... but set it anyway
|
||||
dv_c, c_c = self.handler._challenge_factory("0", [1, 3])
|
||||
self.handler.dv_c["0"], self.handler.cont_c["0"] = dv_c, c_c
|
||||
dv_c, c_c = self.handler._challenge_factory("1", [2])
|
||||
self.handler.dv_c["1"], self.handler.cont_c["1"] = dv_c, c_c
|
||||
|
||||
self.iteration += 1
|
||||
|
||||
elif self.iteration == 1:
|
||||
# Quick check to make sure it was actually completed.
|
||||
self.assertEqual(
|
||||
self.mock_acme_auth.call_args_list, [mock.call("1")])
|
||||
self.handler.responses["0"][1] = "now_finish"
|
||||
self.handler.responses["0"][3] = "finally!"
|
||||
|
||||
else:
|
||||
raise errors.LetsEncryptAuthHandlerError(
|
||||
"Failed incremental test: too many invocations")
|
||||
|
||||
def _test_finished(self):
|
||||
self.assertFalse(self.handler.msgs)
|
||||
self.assertFalse(self.handler.dv_c)
|
||||
self.assertFalse(self.handler.responses)
|
||||
self.assertFalse(self.handler.paths)
|
||||
self.assertFalse(self.handler.domains)
|
||||
def _validate_all(self, unused1, unused2):
|
||||
for dom in self.handler.authzr.keys():
|
||||
azr = self.handler.authzr[dom]
|
||||
self.handler.authzr[dom] = acme_util.gen_authzr(
|
||||
"valid", dom, [challb.chall for challb in azr.body.challenges],
|
||||
["valid"]*len(azr.body.challenges), azr.body.combinations)
|
||||
|
||||
|
||||
class GenChallengePathTest(unittest.TestCase):
|
||||
@@ -481,42 +177,42 @@ class GenChallengePathTest(unittest.TestCase):
|
||||
logging.disable(logging.NOTSET)
|
||||
|
||||
@classmethod
|
||||
def _call(cls, challs, preferences, combinations):
|
||||
def _call(cls, challbs, preferences, combinations):
|
||||
from letsencrypt.client.auth_handler import gen_challenge_path
|
||||
return gen_challenge_path(challs, preferences, combinations)
|
||||
return gen_challenge_path(challbs, preferences, combinations)
|
||||
|
||||
def test_common_case(self):
|
||||
"""Given DVSNI and SimpleHTTPS with appropriate combos."""
|
||||
challs = (acme_util.DVSNI, acme_util.SIMPLE_HTTPS)
|
||||
challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTPS_P)
|
||||
prefs = [challenges.DVSNI]
|
||||
combos = ((0,), (1,))
|
||||
|
||||
# Smart then trivial dumb path test
|
||||
self.assertEqual(self._call(challs, prefs, combos), (0,))
|
||||
self.assertTrue(self._call(challs, prefs, None))
|
||||
self.assertEqual(self._call(challbs, prefs, combos), (0,))
|
||||
self.assertTrue(self._call(challbs, prefs, None))
|
||||
# Rearrange order...
|
||||
self.assertEqual(self._call(challs[::-1], prefs, combos), (1,))
|
||||
self.assertTrue(self._call(challs[::-1], prefs, None))
|
||||
self.assertEqual(self._call(challbs[::-1], prefs, combos), (1,))
|
||||
self.assertTrue(self._call(challbs[::-1], prefs, None))
|
||||
|
||||
def test_common_case_with_continuity(self):
|
||||
challs = (acme_util.RECOVERY_TOKEN,
|
||||
acme_util.RECOVERY_CONTACT,
|
||||
acme_util.DVSNI,
|
||||
acme_util.SIMPLE_HTTPS)
|
||||
challbs = (acme_util.RECOVERY_TOKEN_P,
|
||||
acme_util.RECOVERY_CONTACT_P,
|
||||
acme_util.DVSNI_P,
|
||||
acme_util.SIMPLE_HTTPS_P)
|
||||
prefs = [challenges.RecoveryToken, challenges.DVSNI]
|
||||
combos = acme_util.gen_combos(challs)
|
||||
self.assertEqual(self._call(challs, prefs, combos), (0, 2))
|
||||
combos = acme_util.gen_combos(challbs)
|
||||
self.assertEqual(self._call(challbs, prefs, combos), (0, 2))
|
||||
|
||||
# dumb_path() trivial test
|
||||
self.assertTrue(self._call(challs, prefs, None))
|
||||
self.assertTrue(self._call(challbs, prefs, None))
|
||||
|
||||
def test_full_cont_server(self):
|
||||
challs = (acme_util.RECOVERY_TOKEN,
|
||||
acme_util.RECOVERY_CONTACT,
|
||||
acme_util.POP,
|
||||
acme_util.DVSNI,
|
||||
acme_util.SIMPLE_HTTPS,
|
||||
acme_util.DNS)
|
||||
challbs = (acme_util.RECOVERY_TOKEN_P,
|
||||
acme_util.RECOVERY_CONTACT_P,
|
||||
acme_util.POP_P,
|
||||
acme_util.DVSNI_P,
|
||||
acme_util.SIMPLE_HTTPS_P,
|
||||
acme_util.DNS_P)
|
||||
# Typical webserver client that can do everything except DNS
|
||||
# Attempted to make the order realistic
|
||||
prefs = [challenges.RecoveryToken,
|
||||
@@ -524,19 +220,19 @@ class GenChallengePathTest(unittest.TestCase):
|
||||
challenges.SimpleHTTPS,
|
||||
challenges.DVSNI,
|
||||
challenges.RecoveryContact]
|
||||
combos = acme_util.gen_combos(challs)
|
||||
self.assertEqual(self._call(challs, prefs, combos), (0, 4))
|
||||
combos = acme_util.gen_combos(challbs)
|
||||
self.assertEqual(self._call(challbs, prefs, combos), (0, 4))
|
||||
|
||||
# Dumb path trivial test
|
||||
self.assertTrue(self._call(challs, prefs, None))
|
||||
self.assertTrue(self._call(challbs, prefs, None))
|
||||
|
||||
def test_not_supported(self):
|
||||
challs = (acme_util.POP, acme_util.DVSNI)
|
||||
challbs = (acme_util.POP_P, acme_util.DVSNI_P)
|
||||
prefs = [challenges.DVSNI]
|
||||
combos = ((0, 1),)
|
||||
|
||||
self.assertRaises(errors.LetsEncryptAuthHandlerError,
|
||||
self._call, challs, prefs, combos)
|
||||
self.assertRaises(errors.AuthorizationError,
|
||||
self._call, challbs, prefs, combos)
|
||||
|
||||
|
||||
class MutuallyExclusiveTest(unittest.TestCase):
|
||||
@@ -595,15 +291,15 @@ class IsPreferredTest(unittest.TestCase):
|
||||
]))
|
||||
|
||||
def test_empty_satisfied(self):
|
||||
self.assertTrue(self._call(acme_util.DNS, frozenset()))
|
||||
self.assertTrue(self._call(acme_util.DNS_P, frozenset()))
|
||||
|
||||
def test_mutually_exclusvie(self):
|
||||
self.assertFalse(
|
||||
self._call(acme_util.DVSNI, frozenset([acme_util.SIMPLE_HTTPS])))
|
||||
self._call(acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTPS_P])))
|
||||
|
||||
def test_mutually_exclusive_same_type(self):
|
||||
self.assertTrue(
|
||||
self._call(acme_util.DVSNI, frozenset([acme_util.DVSNI])))
|
||||
self._call(acme_util.DVSNI_P, frozenset([acme_util.DVSNI_P])))
|
||||
|
||||
|
||||
def gen_auth_resp(chall_list):
|
||||
@@ -612,6 +308,12 @@ def gen_auth_resp(chall_list):
|
||||
for chall in chall_list]
|
||||
|
||||
|
||||
def gen_dom_authzr(domain, unused_new_authzr_uri, challs):
|
||||
"""Generates new authzr for domains."""
|
||||
return acme_util.gen_authzr(
|
||||
"pending", domain, challs, ["pending"]*len(challs))
|
||||
|
||||
|
||||
def gen_path(required, challs):
|
||||
"""Generate a combination by picking ``required`` from ``challs``.
|
||||
|
||||
@@ -625,5 +327,6 @@ def gen_path(required, challs):
|
||||
"""
|
||||
return [challs.index(chall) for chall in required]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user