diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 9d4dcbf30..0b9ea8105 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -2,12 +2,13 @@ import collections from acme import challenges +from acme import errors from acme import fields from acme import jose from acme import util -class Error(jose.JSONObjectWithFields, Exception): +class Error(jose.JSONObjectWithFields, errors.Error): """ACME error. https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 @@ -17,55 +18,40 @@ class Error(jose.JSONObjectWithFields, Exception): :ivar unicode detail: """ - ERROR_TYPE_NAMESPACE = 'urn:acme:error:' - ERROR_TYPE_DESCRIPTIONS = { - 'badCSR': 'The CSR is unacceptable (e.g., due to a short key)', - 'badNonce': 'The client sent an unacceptable anti-replay nonce', - 'connection': 'The server could not connect to the client for DV', - 'dnssec': 'The server could not validate a DNSSEC signed domain', - 'malformed': 'The request message was malformed', - 'rateLimited': 'There were too many requests of a given type', - 'serverInternal': 'The server experienced an internal error', - 'tls': 'The server experienced a TLS error during DV', - 'unauthorized': 'The client lacks sufficient authorization', - 'unknownHost': 'The server could not resolve a domain name', - } + ERROR_TYPE_DESCRIPTIONS = dict( + ('urn:acme:error:' + name, description) for name, description in ( + ('badCSR', 'The CSR is unacceptable (e.g., due to a short key)'), + ('badNonce', 'The client sent an unacceptable anti-replay nonce'), + ('connection', 'The server could not connect to the client for DV'), + ('dnssec', 'The server could not validate a DNSSEC signed domain'), + ('malformed', 'The request message was malformed'), + ('rateLimited', 'There were too many requests of a given type'), + ('serverInternal', 'The server experienced an internal error'), + ('tls', 'The server experienced a TLS error during DV'), + ('unauthorized', 'The client lacks sufficient authorization'), + ('unknownHost', 'The server could not resolve a domain name'), + ) + ) typ = jose.Field('type') title = jose.Field('title', omitempty=True) detail = jose.Field('detail') - @typ.encoder - def typ(value): # pylint: disable=missing-docstring,no-self-argument - return Error.ERROR_TYPE_NAMESPACE + value - - @typ.decoder - def typ(value): # pylint: disable=missing-docstring,no-self-argument - # pylint thinks isinstance(value, Error), so startswith is not found - # pylint: disable=no-member - if not value.startswith(Error.ERROR_TYPE_NAMESPACE): - raise jose.DeserializationError('Missing error type prefix') - - without_prefix = value[len(Error.ERROR_TYPE_NAMESPACE):] - if without_prefix not in Error.ERROR_TYPE_DESCRIPTIONS: - raise jose.DeserializationError('Error type not recognized') - - return without_prefix - @property def description(self): """Hardcoded error description based on its type. + :returns: Description if standard ACME error or ``None``. :rtype: unicode """ - return self.ERROR_TYPE_DESCRIPTIONS[self.typ] + return self.ERROR_TYPE_DESCRIPTIONS.get(self.typ) def __str__(self): - if self.typ is not None: - return ' :: '.join([self.typ, self.description, self.detail]) - else: - return str(self.detail) + return ' :: '.join( + part for part in + (self.typ, self.description, self.detail, self.title) + if part is not None) class _Constant(jose.JSONDeSerializable, collections.Hashable): diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index 6c1c4f596..5a7a71299 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -18,41 +18,30 @@ class ErrorTest(unittest.TestCase): def setUp(self): from acme.messages import Error - self.error = Error(detail='foo', typ='malformed', title='title') - self.jobj = {'detail': 'foo', 'title': 'some title'} - - def test_typ_prefix(self): - self.assertEqual('malformed', self.error.typ) - self.assertEqual( - 'urn:acme:error:malformed', self.error.to_partial_json()['type']) - self.assertEqual( - 'malformed', self.error.from_json(self.error.to_partial_json()).typ) - - def test_typ_decoder_missing_prefix(self): - from acme.messages import Error - self.jobj['type'] = 'malformed' - self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) - self.jobj['type'] = 'not valid bare type' - self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) - - def test_typ_decoder_not_recognized(self): - from acme.messages import Error - self.jobj['type'] = 'urn:acme:error:baz' - self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) - - def test_description(self): - self.assertEqual( - 'The request message was malformed', self.error.description) + self.error = Error( + detail='foo', typ='urn:acme:error:malformed', title='title') + self.jobj = { + 'detail': 'foo', + 'title': 'some title', + 'type': 'urn:acme:error:malformed', + } + self.error_custom = Error(typ='custom', detail='bar') + self.jobj_cusom = {'type': 'custom', 'detail': 'bar'} def test_from_json_hashable(self): from acme.messages import Error hash(Error.from_json(self.error.to_json())) + def test_description(self): + self.assertEqual( + 'The request message was malformed', self.error.description) + self.assertTrue(self.error_custom.description is None) + def test_str(self): self.assertEqual( - 'malformed :: The request message was malformed :: foo', - str(self.error)) - self.assertEqual('foo', str(self.error.update(typ=None))) + 'urn:acme:error:malformed :: The request message was ' + 'malformed :: foo :: title', str(self.error)) + self.assertEqual('custom :: bar', str(self.error_custom)) class ConstantTest(unittest.TestCase): @@ -232,7 +221,7 @@ class ChallengeBodyTest(unittest.TestCase): from acme.messages import Error from acme.messages import STATUS_INVALID self.status = STATUS_INVALID - error = Error(typ='serverInternal', + error = Error(typ='urn:acme:error:serverInternal', detail='Unable to communicate with DNS server') self.challb = ChallengeBody( uri='http://challb', chall=self.chall, status=self.status,