From 2b8f2cc1130976ce8d14cd69388c5d5c588feb39 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 28 Feb 2015 20:40:33 +0000 Subject: [PATCH 01/92] rm letsencrypt.py, chmod -x, remove sheebangs --- letsencrypt.py | 1 - letsencrypt/client/standalone_authenticator.py | 0 letsencrypt/scripts/main.py | 1 - setup.py | 1 - 4 files changed, 3 deletions(-) delete mode 120000 letsencrypt.py mode change 100755 => 100644 letsencrypt/client/standalone_authenticator.py mode change 100755 => 100644 letsencrypt/scripts/main.py mode change 100755 => 100644 setup.py diff --git a/letsencrypt.py b/letsencrypt.py deleted file mode 120000 index 77b93ee70..000000000 --- a/letsencrypt.py +++ /dev/null @@ -1 +0,0 @@ -letsencrypt/scripts/main.py \ No newline at end of file diff --git a/letsencrypt/client/standalone_authenticator.py b/letsencrypt/client/standalone_authenticator.py old mode 100755 new mode 100644 diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py old mode 100755 new mode 100644 index 989e07f96..d1df56c09 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Parse command line and call the appropriate functions. .. todo:: Sanity check all input. Be sure to avoid shell code etc... diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 1fc643304..60d68f4a1 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python import codecs import os import re From 7d41cadc99532906253a98d8ef8867889af21380 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 16 Mar 2015 22:58:33 -0700 Subject: [PATCH 02/92] make _path_satisfied conform to API --- letsencrypt/client/auth_handler.py | 4 +++- letsencrypt/client/tests/auth_handler_test.py | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index 4e3b5f68f..980a7d7cd 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -203,7 +203,9 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes def _path_satisfied(self, dom): """Returns whether a path has been completely satisfied.""" - return all(self.responses[dom][i] is not None for i in self.paths[dom]) + return all( + self.responses[dom][i] is not None and + self.responses[dom][i] is not False for i in self.paths[dom]) def _get_chall_pref(self, domain): """Return list of challenge preferences. diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index 91874dc0c..734f4cc65 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -481,7 +481,7 @@ class PathSatisfiedTest(unittest.TestCase): self.handler.responses[dom[0]] = [None, "sat", "sat2", None] self.handler.paths[dom[1]] = [0] - self.handler.responses[dom[1]] = ["sat", None, None, None] + self.handler.responses[dom[1]] = ["sat", None, None, False] self.handler.paths[dom[2]] = [0] self.handler.responses[dom[2]] = ["sat"] @@ -496,7 +496,7 @@ class PathSatisfiedTest(unittest.TestCase): self.assertTrue(self.handler._path_satisfied(dom[i])) def test_not_satisfied(self): - dom = ["0", "1", "2"] + dom = ["0", "1", "2", "3"] self.handler.paths[dom[0]] = [1, 2] self.handler.responses[dom[0]] = ["sat1", None, "sat2", None] @@ -506,6 +506,9 @@ class PathSatisfiedTest(unittest.TestCase): self.handler.paths[dom[2]] = [0] self.handler.responses[dom[2]] = [None] + self.handler.paths[dom[2]] = [0] + self.handler.responses[dom[2]] = [False] + for i in xrange(3): self.assertFalse(self.handler._path_satisfied(dom[i])) From b47cc8eb8f27b3f5f56b15afdcf425f195ac94c7 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 16 Mar 2015 23:00:31 -0700 Subject: [PATCH 03/92] fix _path_satisfied test --- letsencrypt/client/tests/auth_handler_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index 734f4cc65..e0169ab15 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -506,8 +506,8 @@ class PathSatisfiedTest(unittest.TestCase): self.handler.paths[dom[2]] = [0] self.handler.responses[dom[2]] = [None] - self.handler.paths[dom[2]] = [0] - self.handler.responses[dom[2]] = [False] + self.handler.paths[dom[3]] = [0] + self.handler.responses[dom[3]] = [False] for i in xrange(3): self.assertFalse(self.handler._path_satisfied(dom[i])) From 7e820b093d100744e77a8928fbe3c44a4e7676b5 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 20 Mar 2015 23:58:23 +0000 Subject: [PATCH 04/92] Initial impl. of v02, works with Boulder --- examples/restified.py | 66 ++++++++++++++++ letsencrypt/acme/messages2.py | 139 ++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 examples/restified.py create mode 100644 letsencrypt/acme/messages2.py diff --git a/examples/restified.py b/examples/restified.py new file mode 100644 index 000000000..b4bd6c842 --- /dev/null +++ b/examples/restified.py @@ -0,0 +1,66 @@ +import httplib +import logging +import os +import pkg_resources +import requests + +from letsencrypt.acme import messages2 +from letsencrypt.acme import jose + + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) + +URL_ROOT = 'https://www.letsencrypt-demo.org' +NEW_AUTHZ_URL = URL_ROOT + '/acme/new-authz' +NEW_CERT_URL = URL_ROOT + '/acme/new-certz' + + +class Resource(jose.ImmutableMap): + __slots__ = ('body', 'location') + + +def send(resource, key, alg=jose.RS256): + dumps = resource.body.json_dumps() + logging.debug('Serialized JSON: %s', dumps) + sig = jose.JWS.sign(payload=dumps, key=key, alg=alg).json_dumps() + logging.debug('Serialized JWS: %s', sig) + + response = requests.post(resource.location, sig) + logging.debug('Received response %s: %s', response, response.text) + + if (response.status_code == httplib.OK or + response.status_code == httplib.CREATED): + pass + + # TODO: server might override NEW_AUTHZ_URI (after new-reg) or + # NEW_CERTZ_URI (after new-authz) and we should use it + # instead. Below code only prints the link. + if 'next' in response.links: + logging.debug('Link (next): %s', response.links['next']['url']) + if 'up' in response.links: + logging.debug('Link (up): %s', response.links['up']['url']) + + # TODO: new-cert response is not JSON + return Resource( + body=type(resource.body).from_json(response.json()), + location=response.headers['location']) + + +registration = messages2.Registration(contact=( + 'mailto:cert-admin@example.com', 'tel:+12025551212')) +key = jose.JWKRSA.load(pkg_resources.resource_string( + 'letsencrypt.acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) + +authz = Resource(body=messages2.Authorization(identifier=messages2.Identifier( + typ=messages2.Identifier.FQDN, value="example1.com")), + location=NEW_AUTHZ_URL) + +authz2 = send(authz, key) +assert authz2.body.key == key.public() +assert authz2.body.identifier == authz.body.identifier +assert authz2.body.challenges is not None + +print authz2 +print +print requests.get(authz2.location).json() diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py new file mode 100644 index 000000000..3213e9aa3 --- /dev/null +++ b/letsencrypt/acme/messages2.py @@ -0,0 +1,139 @@ +"""ACME protocol v02 messages.""" +import jsonschema + +from letsencrypt.acme import challenges +from letsencrypt.acme import errors +from letsencrypt.acme import jose +from letsencrypt.acme import other +from letsencrypt.acme import util + + +class Resource(jose.JSONObjectWithFields): + """ACME Resource.""" + + +class Error(object): + """ACME error. + + 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", + "unauthorized": "The client lacks sufficient authorization", + "serverInternal": "The server experienced an internal error", + "badCSR": "The CSR is unacceptable (e.g., due to a short key)", + } + + typ = jose.Field('type') + title = jose.Field('title', omitempty=True) + detail = jose.Field('detail') + instance = jose.Field('instance') + + @typ.encoder + def typ(value): + return ERROR_TYPE_NAMESPACE + value + + @typ.decoder + def typ(value): + if not value.startswith(ERROR_TYPE_NAMESPACE): + raise errors.DeserializationError('Unrecognized error type') + + return value[len(ERROR_TYPE_NAMESPACE):] + + @property + def description(self): + return self.ERROR_TYPE_DESCRIPTIONS[self.typ] + + +class Registration(Resource): + """Registration resource.""" + + # key will be ignored by server and taken from JWS instead + key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) + contact = jose.Field('contact', omitempty=True, default=()) + recovery_token = jose.Field('recoveryToken', omitempty=True) + + +class Identifier(jose.JSONObjectWithFields): + typ = jose.Field('type') + value = jose.Field('value') + + FQDN = 'dns' # TODO: acme-spec uses 'domain' in some examples, + # Boulder uses 'dns' though + +class ChallengeWithMeta(jose.JSONObjectWithFields): + + __slots__ = ('body',) + status = jose.Field('status') + validated = jose.Field('validated', omitempty=True) + uri = jose.Field('uri') + + def to_json(self): + jobj = super(ChallengeWithMeta, self).to_json() + jobj.update(self.body.to_json()) + return jobj + + @classmethod + def fields_from_json(cls, jobj): + fields = super(ChallengeWithMeta, cls).fields_from_json(jobj) + fields['body'] = challenges.Challenge.from_json(jobj) + return fields + +class Authorization(Resource): + class Status(object): + VALID = frozenset(['pending', 'valid', 'invalid']) + + identifier = jose.Field('identifier', decoder=Identifier.from_json) + + # acme-spec marks 'key' as 'required', but new-authz does not need + # to carry it, server will take 'key' from the 'jwk' found in the + # JWS + key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) + status = jose.Field('status', omitempty=True) + challenges = jose.Field('challenges', omitempty=True) + combinations = jose.Field('combinations', omitempty=True) + + # TODO: 'The client MAY provide contact information in the + # "contact" field in this or any subsequent request.' ??? + + # TODO: 'expires' is allowed for Authorization Resources in + # general, but for Authorization '[t]he "expires" field MUST be + # absent'... then acme-spec gives example with 'expires' + # present... That's confusing! + #expires = jose.Field('expires', omitempty=True) + + @property + def resolved_combinations(self): + """Combinations with challenges instead of indices.""" + return tuple(tuple(self.challenges[idx] for idx in combo) + for combo in self.combinations) + + @challenges.decoder + def challenges(value): # pylint: disable=missing-docstring,no-self-argument + # TODO: acme-spec examples use hybrid between a list and a + # dict: "challenges": [ "simpleHttps": {}, ... ], while + # Boulder uses (more sane): "challenges": [{"type": + # "simpleHttps", ...}, ...] + + # TODO: Server also returns the follwing: + # u'status': u'pending', u'completed': u'0001-01-01T00:00:00Z' + # "uri":"http://0.0.0.0:4000/acme/authz/vI_H5tJroyaGhappi8xBtpGYSYBvuIo3JIvakORaEJo?challenge=0" + tuple((chall['status'], chall.get('validated'), chall['uri']) + for chall in value) + + return tuple(ChallengeWithMeta.from_json(chall) for chall in value) + + +class NewCertificate(Resource): + """ACME new certificate resource request.""" + + csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) + authorizations = jose.Field('authorizations', decoder=tuple) + + +class Revocation(Resource): + revoke = jose.Field('revoke') + authorizations = NewCertificate.authorizations From 603f891a375c4022068bfd0cff20bca7fa27fff5 Mon Sep 17 00:00:00 2001 From: William Budington Date: Sat, 21 Mar 2015 20:01:41 +0000 Subject: [PATCH 05/92] Renaming ClientAuthenticator to ContinuityAuthenticator --- letsencrypt/client/client.py | 6 +++--- ...nt_authenticator.py => continuity_authenticator.py} | 2 +- letsencrypt/client/tests/auth_handler_test.py | 4 ++-- letsencrypt/client/tests/client_authenticator_test.py | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) rename letsencrypt/client/{client_authenticator.py => continuity_authenticator.py} (97%) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index d415403f3..25a1cc1f6 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -10,7 +10,7 @@ from letsencrypt.acme import messages from letsencrypt.acme import util as acme_util from letsencrypt.client import auth_handler -from letsencrypt.client import client_authenticator +from letsencrypt.client import continuity_authenticator from letsencrypt.client import crypto_util from letsencrypt.client import errors from letsencrypt.client import le_util @@ -33,7 +33,7 @@ class Client(object): :type authkey: :class:`letsencrypt.client.le_util.Key` :ivar auth_handler: Object that supports the IAuthenticator interface. - auth_handler contains both a dv_authenticator and a client_authenticator + auth_handler contains both a dv_authenticator and a continuity_authenticator :type auth_handler: :class:`letsencrypt.client.auth_handler.AuthHandler` :ivar installer: Object supporting the IInstaller interface. @@ -60,7 +60,7 @@ class Client(object): self.config = config if dv_auth is not None: - client_auth = client_authenticator.ClientAuthenticator(config) + client_auth = continuity_authenticator.ContinuityAuthenticator(config) self.auth_handler = auth_handler.AuthHandler( dv_auth, client_auth, self.network) else: diff --git a/letsencrypt/client/client_authenticator.py b/letsencrypt/client/continuity_authenticator.py similarity index 97% rename from letsencrypt/client/client_authenticator.py rename to letsencrypt/client/continuity_authenticator.py index 3cef97355..af979a7c2 100644 --- a/letsencrypt/client/client_authenticator.py +++ b/letsencrypt/client/continuity_authenticator.py @@ -9,7 +9,7 @@ from letsencrypt.client import interfaces from letsencrypt.client import recovery_token -class ClientAuthenticator(object): +class ContinuityAuthenticator(object): """IAuthenticator for :const:`~letsencrypt.client.constants.CLIENT_CHALLENGES`. diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index 91874dc0c..3349ebdf9 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -30,7 +30,7 @@ class SatisfyChallengesTest(unittest.TestCase): from letsencrypt.client.auth_handler import AuthHandler self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator") - self.mock_client_auth = mock.MagicMock(name="ClientAuthenticator") + self.mock_client_auth = mock.MagicMock(name="ContinuityAuthenticator") self.mock_dv_auth.get_chall_pref.return_value = [challenges.DVSNI] self.mock_client_auth.get_chall_pref.return_value = [ @@ -346,7 +346,7 @@ class GetAuthorizationsTest(unittest.TestCase): from letsencrypt.client.auth_handler import AuthHandler self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator") - self.mock_client_auth = mock.MagicMock(name="ClientAuthenticator") + self.mock_client_auth = mock.MagicMock(name="ContinuityAuthenticator") self.mock_sat_chall = mock.MagicMock(name="_satisfy_challenges") self.mock_acme_auth = mock.MagicMock(name="acme_authorization") diff --git a/letsencrypt/client/tests/client_authenticator_test.py b/letsencrypt/client/tests/client_authenticator_test.py index 7db1956d5..1f1d8f3f8 100644 --- a/letsencrypt/client/tests/client_authenticator_test.py +++ b/letsencrypt/client/tests/client_authenticator_test.py @@ -1,4 +1,4 @@ -"""Test the ClientAuthenticator dispatcher.""" +"""Test the ContinuityAuthenticator dispatcher.""" import unittest import mock @@ -13,9 +13,9 @@ class PerformTest(unittest.TestCase): """Test client perform function.""" def setUp(self): - from letsencrypt.client.client_authenticator import ClientAuthenticator + from letsencrypt.client.continuity_authenticator import ContinuityAuthenticator - self.auth = ClientAuthenticator( + self.auth = ContinuityAuthenticator( mock.MagicMock(server="demo_server.org")) self.auth.rec_token.perform = mock.MagicMock( name="rec_token_perform", side_effect=gen_client_resp) @@ -50,9 +50,9 @@ class CleanupTest(unittest.TestCase): """Test the Authenticator cleanup function.""" def setUp(self): - from letsencrypt.client.client_authenticator import ClientAuthenticator + from letsencrypt.client.continuity_authenticator import ContinuityAuthenticator - self.auth = ClientAuthenticator( + self.auth = ContinuityAuthenticator( mock.MagicMock(server="demo_server.org")) self.mock_cleanup = mock.MagicMock(name="rec_token_cleanup") self.auth.rec_token.cleanup = self.mock_cleanup From f1081a3d68aa1de863baca1dd38959b25128198d Mon Sep 17 00:00:00 2001 From: William Budington Date: Sat, 21 Mar 2015 22:24:35 +0000 Subject: [PATCH 06/92] Rename test filename as well --- ...ent_authenticator_test.py => continuity_authenticator_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename letsencrypt/client/tests/{client_authenticator_test.py => continuity_authenticator_test.py} (100%) diff --git a/letsencrypt/client/tests/client_authenticator_test.py b/letsencrypt/client/tests/continuity_authenticator_test.py similarity index 100% rename from letsencrypt/client/tests/client_authenticator_test.py rename to letsencrypt/client/tests/continuity_authenticator_test.py From 1a0af51f6f6133116f33f3668ac3a2a5e7db230f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 23 Mar 2015 08:25:44 +0000 Subject: [PATCH 07/92] Fix Sphinx M2Crypto.X509 import errors --- letsencrypt/acme/jose/jws.py | 2 +- letsencrypt/acme/jose/jws_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/acme/jose/jws.py b/letsencrypt/acme/jose/jws.py index 81106dd2c..3b962aede 100644 --- a/letsencrypt/acme/jose/jws.py +++ b/letsencrypt/acme/jose/jws.py @@ -3,7 +3,7 @@ import argparse import base64 import sys -import M2Crypto.X509 +import M2Crypto from letsencrypt.acme.jose import b64 from letsencrypt.acme.jose import errors diff --git a/letsencrypt/acme/jose/jws_test.py b/letsencrypt/acme/jose/jws_test.py index 6e6b51350..215960e15 100644 --- a/letsencrypt/acme/jose/jws_test.py +++ b/letsencrypt/acme/jose/jws_test.py @@ -5,7 +5,7 @@ import pkg_resources import unittest import Crypto.PublicKey.RSA -import M2Crypto.X509 +import M2Crypto import mock from letsencrypt.acme.jose import b64 From 53bdf5e24627cf94526a3c12f30e48b164d891e7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 23 Mar 2015 08:27:57 +0000 Subject: [PATCH 08/92] Fix _enable_redirect docstring --- letsencrypt/client/apache/configurator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/letsencrypt/client/apache/configurator.py b/letsencrypt/client/apache/configurator.py index 93db689f8..89a2ff4e2 100644 --- a/letsencrypt/client/apache/configurator.py +++ b/letsencrypt/client/apache/configurator.py @@ -548,8 +548,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): def _enable_redirect(self, ssl_vhost, unused_options): """Redirect all equivalent HTTP traffic to ssl_vhost. - .. todo:: This enhancement should be rewritten and will unfortunately - require lots of debugging by hand. + .. todo:: This enhancement should be rewritten and will + unfortunately require lots of debugging by hand. + Adds Redirect directive to the port 80 equivalent of ssl_vhost First the function attempts to find the vhost with equivalent ip addresses that serves on non-ssl ports From 533cfa42c74fa0f314805ea22846cda2e294615d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 23 Mar 2015 08:35:36 +0000 Subject: [PATCH 09/92] MANIFEST: Update CONTRIBUTING extension --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index bea6fd9bb..3bd657b87 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include README.rst include CHANGES.rst -include CONTRIBUTING.rst +include CONTRIBUTING.md include linter_plugin.py include letsencrypt/EULA recursive-include letsencrypt *.json From 71e17df03a5f9f3d87a244d7fd9656c0d2b8285d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 23 Mar 2015 09:19:13 +0000 Subject: [PATCH 10/92] InsecurePlatformWarning (fixes #304) --- letsencrypt/client/network.py | 3 +++ setup.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/letsencrypt/client/network.py b/letsencrypt/client/network.py index de6db575b..2719583c3 100644 --- a/letsencrypt/client/network.py +++ b/letsencrypt/client/network.py @@ -11,6 +11,9 @@ from letsencrypt.acme import messages from letsencrypt.client import errors +# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning +requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() + logging.getLogger("requests").setLevel(logging.WARNING) diff --git a/setup.py b/setup.py index 520147433..45873e9e8 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,9 @@ install_requires = [ 'ConfArgParse', 'jsonschema', 'mock', + 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'psutil>=2.1.0', # net_connections introduced in 2.1.0 + 'pyasn1', # urllib3 InsecurePlatformWarning (#304) 'pycrypto', 'PyOpenSSL', 'python-augeas', From c9589d33d3376b20bffc0f19fdbe3114308b929e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 12:34:21 +0000 Subject: [PATCH 11/92] Update messages2, network2 stub, example updated --- examples/restified.py | 63 +++--------- letsencrypt/acme/messages2.py | 183 ++++++++++++++++++++++++--------- letsencrypt/client/network2.py | 67 ++++++++++++ 3 files changed, 221 insertions(+), 92 deletions(-) create mode 100644 letsencrypt/client/network2.py diff --git a/examples/restified.py b/examples/restified.py index b4bd6c842..740441a84 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -1,66 +1,37 @@ -import httplib import logging import os import pkg_resources -import requests from letsencrypt.acme import messages2 from letsencrypt.acme import jose +from letsencrypt.client import network2 + logger = logging.getLogger() logger.setLevel(logging.DEBUG) URL_ROOT = 'https://www.letsencrypt-demo.org' +NEW_REG_URL = URL_ROOT + '/acme/new-reg' NEW_AUTHZ_URL = URL_ROOT + '/acme/new-authz' -NEW_CERT_URL = URL_ROOT + '/acme/new-certz' +#NEW_CERT_URL = URL_ROOT + '/acme/new-certz' -class Resource(jose.ImmutableMap): - __slots__ = ('body', 'location') - - -def send(resource, key, alg=jose.RS256): - dumps = resource.body.json_dumps() - logging.debug('Serialized JSON: %s', dumps) - sig = jose.JWS.sign(payload=dumps, key=key, alg=alg).json_dumps() - logging.debug('Serialized JWS: %s', sig) - - response = requests.post(resource.location, sig) - logging.debug('Received response %s: %s', response, response.text) - - if (response.status_code == httplib.OK or - response.status_code == httplib.CREATED): - pass - - # TODO: server might override NEW_AUTHZ_URI (after new-reg) or - # NEW_CERTZ_URI (after new-authz) and we should use it - # instead. Below code only prints the link. - if 'next' in response.links: - logging.debug('Link (next): %s', response.links['next']['url']) - if 'up' in response.links: - logging.debug('Link (up): %s', response.links['up']['url']) - - # TODO: new-cert response is not JSON - return Resource( - body=type(resource.body).from_json(response.json()), - location=response.headers['location']) - - -registration = messages2.Registration(contact=( - 'mailto:cert-admin@example.com', 'tel:+12025551212')) key = jose.JWKRSA.load(pkg_resources.resource_string( 'letsencrypt.acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) +net = network2.Network(NEW_REG_URL, key) -authz = Resource(body=messages2.Authorization(identifier=messages2.Identifier( - typ=messages2.Identifier.FQDN, value="example1.com")), - location=NEW_AUTHZ_URL) +contact = contact=('mailto:cert-admin@example.com', 'tel:+12025551212') +# Boulder does not support registrations +#regr = net.register(contact=contact) +regr = messages2.RegistrationResource( + body=messages2.Registration(contact=contact, key=key.public()), + uri=NEW_REG_URL + '/fooooo', + new_authz_uri=NEW_AUTHZ_URL) -authz2 = send(authz, key) -assert authz2.body.key == key.public() -assert authz2.body.identifier == authz.body.identifier -assert authz2.body.challenges is not None +authzr = net.request_challenges( + identifier=messages2.Identifier( + typ=messages2.IdentifierFQDN, value="example1.com"), + regr=regr) -print authz2 -print -print requests.get(authz2.location).json() +print authzr diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 3213e9aa3..b208639e8 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -8,11 +8,7 @@ from letsencrypt.acme import other from letsencrypt.acme import util -class Resource(jose.JSONObjectWithFields): - """ACME Resource.""" - - -class Error(object): +class Error(jose.JSONObjectWithFields): """ACME error. https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 @@ -48,43 +44,129 @@ class Error(object): return self.ERROR_TYPE_DESCRIPTIONS[self.typ] -class Registration(Resource): +class _Constant(jose.JSONDeSerializable): + """ACME constant.""" + __slots__ = ('name',) + POSSIBLE_NAMES = NotImplemented + + def __init__(self, name): + self.POSSIBLE_NAMES[name] = self + self.name = name + + def to_json(self): + return self.name + + @classmethod + def from_json(cls, value): + if value not in cls.POSSIBLE_NAMES: + raise jose.DeserializationError( + '{} not recognized'.format(cls.__name__)) + return cls.POSSIBLE_NAMES[value] + + def __repr__(self): + return '{0}({0})'.format(self.__class__.__name__, self.name) + + def __eq__(self, other): + return isinstance(other, type(self)) and other.name == self.name + + +class Status(_Constant): + """ACME "status" field.""" + POSSIBLE_NAMES = {} +# TODO: acme-spec #88 +StatusUnknown = Status('unknown') +StatusPending = Status('pending') +StatusProcessing = Status('processing') +StatusValid = Status('valid') +StatusInvalid = Status('invalid') +StatusRevoked = Status('revoked') + + +class IdentifierType(_Constant): + """ACME identifier type.""" + POSSIBLE_NAMES = {} +IdentifierFQDN = IdentifierType('dns') # IdentifierDNS in Boulder + +class Identifier(jose.JSONObjectWithFields): + """ACME identifier.""" + typ = jose.Field('type', decoder=IdentifierType.from_json) + value = jose.Field('value') + + +class Resource(jose.ImmutableMap): + """ACME Resource. + + :param body: Resource body. + :type body: Instance of `ResourceBody` (subclass). + + :param str uri: Location of the resource. + + """ + __slots__ = ('body', 'uri') + + +class ResourceBody(jose.JSONObjectWithFields): + """ACME Resource body.""" + + +class RegistrationResource(Resource): + """Registration resource. + + :ivar body: `Registration` + :ivar str uri: URI of the resource. + :ivar new_authz_uri: URI found in the 'next' Link header + + """ + __slots__ = ('body', 'uri', 'new_authz_uri') + + +class Registration(ResourceBody): """Registration resource.""" - # key will be ignored by server and taken from JWS instead + # 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) contact = jose.Field('contact', omitempty=True, default=()) recovery_token = jose.Field('recoveryToken', omitempty=True) -class Identifier(jose.JSONObjectWithFields): - typ = jose.Field('type') - value = jose.Field('value') +class ChallengeResource(Resource, jose.JSONObjectWithFields): + """Challenge resource. - FQDN = 'dns' # TODO: acme-spec uses 'domain' in some examples, - # Boulder uses 'dns' though + :ivar body: `.challenges.Challenge` + :ivar authz_uri: URI found in the 'up' Link header. -class ChallengeWithMeta(jose.JSONObjectWithFields): + """ + __slots__ = ('body',)# 'authz_uri') - __slots__ = ('body',) - status = jose.Field('status') - validated = jose.Field('validated', omitempty=True) uri = jose.Field('uri') + status = jose.Field('status', decoder=Status.from_json) + # TODO: de/encode datetime + validated = jose.Field('validated', omitempty=True) def to_json(self): - jobj = super(ChallengeWithMeta, self).to_json() + jobj = super(ChallengeResource, self).to_json() jobj.update(self.body.to_json()) return jobj @classmethod def fields_from_json(cls, jobj): - fields = super(ChallengeWithMeta, cls).fields_from_json(jobj) + fields = super(ChallengeResource, cls).fields_from_json(jobj) fields['body'] = challenges.Challenge.from_json(jobj) return fields -class Authorization(Resource): - class Status(object): - VALID = frozenset(['pending', 'valid', 'invalid']) + +class AuthorizationResource(Resource): + """Authorization resource. + + :ivar body: `Authorization` + :ivar new_cert_uri: URI found in the 'next' Link header + + """ + __slots__ = ('body', 'uri', 'new_cert_uri') + + +class Authorization(ResourceBody): identifier = jose.Field('identifier', decoder=Identifier.from_json) @@ -92,18 +174,23 @@ class Authorization(Resource): # to carry it, server will take 'key' from the 'jwk' found in the # JWS key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) - status = jose.Field('status', omitempty=True) + status = jose.Field('status', omitempty=True, decoder=Status.from_json) challenges = jose.Field('challenges', omitempty=True) - combinations = jose.Field('combinations', omitempty=True) - - # TODO: 'The client MAY provide contact information in the - # "contact" field in this or any subsequent request.' ??? # TODO: 'expires' is allowed for Authorization Resources in # general, but for Authorization '[t]he "expires" field MUST be # absent'... then acme-spec gives example with 'expires' # present... That's confusing! - #expires = jose.Field('expires', omitempty=True) + expires = jose.Field('expires', omitempty=True) # TODO: this is date + + combinations = jose.Field('combinations', omitempty=True) + + # TODO: 'The client MAY provide contact information in the + # "contact" field in this or any subsequent request.' ??? + + @challenges.decoder + def challenges(value): # pylint: disable=missing-docstring,no-self-argument + return tuple(ChallengeResource.from_json(chall) for chall in value) @property def resolved_combinations(self): @@ -111,29 +198,33 @@ class Authorization(Resource): return tuple(tuple(self.challenges[idx] for idx in combo) for combo in self.combinations) - @challenges.decoder - def challenges(value): # pylint: disable=missing-docstring,no-self-argument - # TODO: acme-spec examples use hybrid between a list and a - # dict: "challenges": [ "simpleHttps": {}, ... ], while - # Boulder uses (more sane): "challenges": [{"type": - # "simpleHttps", ...}, ...] - # TODO: Server also returns the follwing: - # u'status': u'pending', u'completed': u'0001-01-01T00:00:00Z' - # "uri":"http://0.0.0.0:4000/acme/authz/vI_H5tJroyaGhappi8xBtpGYSYBvuIo3JIvakORaEJo?challenge=0" - tuple((chall['status'], chall.get('validated'), chall['uri']) - for chall in value) +class CertificateRequest(jose.JSONObjectWithFields): + """ACME new-cert request. - return tuple(ChallengeWithMeta.from_json(chall) for chall in value) - - -class NewCertificate(Resource): - """ACME new certificate resource request.""" + :ivar csr: `M2Crypto.X509.Request` + """ csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) authorizations = jose.Field('authorizations', decoder=tuple) -class Revocation(Resource): - revoke = jose.Field('revoke') - authorizations = NewCertificate.authorizations +class CertificateResource(Resource): + """Authorization resource. + + :ivar body: `M2Crypto.X509.X509` + :ivar cert_chain_uri: URI found in the 'up' Link header + :ivar authzs: List of `Authorization`. + + """ + __slots__ = ('body', 'uri', 'cert_chain_uri', 'authz') + + +class Revocation(jose.JSONObjectWithFields): + """Revocation message.""" + + class When(object): # TODO + pass + + revoke = jose.Field('revoke') # TODO: use When + authorizations = CertificateRequest._fields['authorizations'] diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py new file mode 100644 index 000000000..7774efd6f --- /dev/null +++ b/letsencrypt/client/network2.py @@ -0,0 +1,67 @@ +"""Networking for ACME protocol v02.""" +import httplib +import logging + +import requests + +from letsencrypt.acme import jose +from letsencrypt.acme import messages2 + + +class Network(object): + """ACME networking. + + :ivar str new_reg_uri: Location of new-reg + :ivar key: `.JWK` (private) + :ivar alg: `.JWASignature` + + """ + + def __init__(self, new_reg_uri, key, alg=jose.RS256): + self.new_reg_uri = new_reg_uri + self.key = key + self.alg = alg + + def _wrap_in_jws(self, data): + dumps = data.json_dumps() + logging.debug('Serialized JSON: %s', dumps) + return jose.JWS.sign( + payload=dumps, key=self.key, alg=self.alg).json_dumps() + + def _post(self, uri, data): + logging.debug('Sending data: %s', data) + response = requests.post(uri, data) + logging.debug('Received response %s: %s', response, response.text) + return response + + def register(self, contact=messages2.Registration._fields['contact'].default): + new_reg = messages2.Registration(contact=contact) + response = self._post(self.new_reg_uri, self._wrap_in_jws(new_reg)) + assert response.status_code == httplib.CREATED # TODO: handle errors + regr = messages2.RegistrationResource( + body=messages2.Registration.from_json(response.json()), + uri=response.headers['location'], + new_authz_uri=response.links['next']['url'], + ) + assert regr.body.key == self.key.public() + return regr + + def request_challenges(self, identifier, regr): + """Request challenges. + + :param identifier: Identifier to be challenged. + :type identifier: `.messages2.Identifier` + + :pram regr: Registration resource. + :type regr: `.RegistrationResource` + + """ + new_authz = messages2.Authorization(identifier=identifier) + response = self._post(regr.new_authz_uri, self._wrap_in_jws(new_authz)) + assert response.status_code == httplib.CREATED # TODO: handle errors + authzr = messages2.AuthorizationResource( + body=messages2.Authorization.from_json(response.json()), + uri=response.headers['location'], + new_cert_uri=response.links['next']['url']) + assert authzr.body.key == self.key.public() + return authzr From 62cdf4a2f82b9475fff40f634000e7b6693704ff Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 13:24:20 +0000 Subject: [PATCH 12/92] Add more stub methods to network2 --- letsencrypt/acme/messages2.py | 6 +- letsencrypt/client/network2.py | 126 +++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index b208639e8..ec1d1ad1d 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -214,16 +214,16 @@ class CertificateResource(Resource): :ivar body: `M2Crypto.X509.X509` :ivar cert_chain_uri: URI found in the 'up' Link header - :ivar authzs: List of `Authorization`. + :ivar authzrs: `list` of `AuthorizationResource`. """ - __slots__ = ('body', 'uri', 'cert_chain_uri', 'authz') + __slots__ = ('body', 'uri', 'cert_chain_uri', 'authzrs') class Revocation(jose.JSONObjectWithFields): """Revocation message.""" - class When(object): # TODO + class When(object): # TODO: 'now' or datetime pass revoke = jose.Field('revoke') # TODO: use When diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 7774efd6f..c27d9e40c 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -46,6 +46,18 @@ class Network(object): assert regr.body.key == self.key.public() return regr + def update_registration(self, regr): + """Update registration. + + :pram regr: Registration resource. + :type regr: `.RegistrationResource` + + :returns: Updated registration resource. + :rtype: `.RegistrationResource` + + """ + response = self._post(regr.uri, self._wrap_in_jws(regr.body)) + def request_challenges(self, identifier, regr): """Request challenges. @@ -65,3 +77,117 @@ class Network(object): new_cert_uri=response.links['next']['url']) assert authzr.body.key == self.key.public() return authzr + + # TODO: anything below is also stub, bot not working, not tested at all + + def answer_challenge(self, challr, response): + """Answer challenge. + + :param challr: Corresponding challenge resource. + :type challr: `.ChallengeResource` + + :param response: Challenge response + :type response: `.challenges.ChallengeResponse` + + :returns: Updated challenge resource. + :rtype: `.ChallengeResource` + + """ + response = self._post(challr.uri, self._wrap_in_jws(response)) + assert response.headers['location'] == challr.uri + updated_challr = messages2.ChallengeResource( + body=challenges.Challenge.from_json(response.json()), + uri=challr.uri) + return updated_challr + + def answer_challenges(self, challrs, responses): + """Answer multiple challenges. + + .. note:: This is a convenience function to make integration + with old proto code easier and shall probably be removed + once restification is over. + + """ + return [self.answer_challenge(challr, response) + for challr, response in itertools.izip(challrs, responses)] + + def poll(self, authzr): + """Poll Authorization Resource for status. + + :param authzr: Authorization Resource + :type authzr: `.AuthorizationResource` + + :returns: Updated Authorization Resource and 'Retry-After' + value (0, if such header not provided). + + :rtype: (`.AuthorizationResource`, `int`) + + """ + + def request_issuance(self, csr, authzrs): + """Request issuance. + + :param csr: CSR + :type csr: `M2Crypto.X509.Request` + + :param authzrs: `list` of `.AuthorizationResource` + + """ + req = CertificateRequest( + csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) + response = self._post( + authzrs[0].new_cert_uri, # TODO: acme-spec #90 + self._wrap_in_jws(req)) + # assert content-type: application/pkix-cert + return messages2.CertificateResource( + authzrs=authzrs, + body=M2Crypto.X509.load_der_string(response.text), + cert_chain_uri=response.links['up']['url']) + + def poll_and_request_issuance(self, csr, authzrs, mintime=5): + """Poll and request issuance. + + :param int mintime: Minimum time before next attempt + + """ + waiting = set() + finished = set() + + while waiting: + authzr = waiting.pop() + updated_authzr, retry_after = self.poll(authzr) + if updated_authzr.body.status == messages2.StatusValidated: + finished.add(updated_authzr) + else: + waiting.add(updated_authzr) + # TODO: implement reasonable sleeping! + + return request_issuance(csr, authzrs) + + def check_cert(self, certr): + """Check for new cert. + + :param certr: CertificateResource + :type certr: `.CertificateResource` + + """ + # TODO: acme-spec 5.1 table action should be renamed to + # "refresh cert", and this method integrated with self.refresh + return requests.get(certr.uri) + + def refresh(self, certr): + """Refresh certificate.""" + return self.check_cert(certr) + + def fetch_chain(self, certr): + """Fetch chain for certificate.""" + + def revoke(self, certr, when='now'): + """Revoke certificate. + + :param when: When should the revocation take place. + :type when: `.Revocation.When` + + """ + rev = messages2.Revocation(revoke=when, authorizations=tuple( + authzr.uri for authzr in certr.authzrs)) From a6e1c3ed1771582d3e21f44fb0dc5f2ea8d432f7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 17:02:02 +0000 Subject: [PATCH 13/92] Current Boulder supports registrations --- examples/restified.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/examples/restified.py b/examples/restified.py index 740441a84..a769233e8 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -21,13 +21,9 @@ key = jose.JWKRSA.load(pkg_resources.resource_string( 'letsencrypt.acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) net = network2.Network(NEW_REG_URL, key) -contact = contact=('mailto:cert-admin@example.com', 'tel:+12025551212') -# Boulder does not support registrations -#regr = net.register(contact=contact) -regr = messages2.RegistrationResource( - body=messages2.Registration(contact=contact, key=key.public()), - uri=NEW_REG_URL + '/fooooo', - new_authz_uri=NEW_AUTHZ_URL) +regr = net.register(contact=( + 'mailto:cert-admin@example.com', 'tel:+12025551212')) +logging.debug(regr) authzr = net.request_challenges( identifier=messages2.Identifier( From 2b4b86a41bbceefbbe41b380b6ed6efec290aad8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 17:07:52 +0000 Subject: [PATCH 14/92] Registration: TOS and agreement --- letsencrypt/acme/messages2.py | 3 ++- letsencrypt/client/network2.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index ec1d1ad1d..37a384aa4 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -117,7 +117,7 @@ class RegistrationResource(Resource): :ivar new_authz_uri: URI found in the 'next' Link header """ - __slots__ = ('body', 'uri', 'new_authz_uri') + __slots__ = ('body', 'uri', 'new_authz_uri', 'terms_of_service') class Registration(ResourceBody): @@ -128,6 +128,7 @@ class Registration(ResourceBody): key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) contact = jose.Field('contact', omitempty=True, default=()) recovery_token = jose.Field('recoveryToken', omitempty=True) + agreement = jose.Field('agreement', omitempty=True) class ChallengeResource(Resource, jose.JSONObjectWithFields): diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index c27d9e40c..8bfc12a15 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -36,14 +36,19 @@ class Network(object): def register(self, contact=messages2.Registration._fields['contact'].default): new_reg = messages2.Registration(contact=contact) + response = self._post(self.new_reg_uri, self._wrap_in_jws(new_reg)) assert response.status_code == httplib.CREATED # TODO: handle errors + + terms_of_service = (response.links['next']['url'] + if 'terms-of-service' in response.links else None) regr = messages2.RegistrationResource( body=messages2.Registration.from_json(response.json()), uri=response.headers['location'], new_authz_uri=response.links['next']['url'], - ) + terms_of_service=terms_of_service) assert regr.body.key == self.key.public() + return regr def update_registration(self, regr): From 144baf64fe482fc37210479f55f0205508a8e356 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 17:08:25 +0000 Subject: [PATCH 15/92] client.errors.UnexpectedUpdate --- letsencrypt/client/errors.py | 8 ++++++++ letsencrypt/client/network2.py | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/letsencrypt/client/errors.py b/letsencrypt/client/errors.py index c1d6c785f..f924f735a 100644 --- a/letsencrypt/client/errors.py +++ b/letsencrypt/client/errors.py @@ -5,6 +5,14 @@ class LetsEncryptClientError(Exception): """Generic Let's Encrypt client error.""" +class NetworkError(LetsEncryptClientError): + """Network error.""" + + +class UnexpectedUpdate(NetworkError): + """Unexpected update.""" + + class LetsEncryptReverterError(LetsEncryptClientError): """Let's Encrypt Reverter error.""" diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 8bfc12a15..5755d25d3 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -7,6 +7,8 @@ import requests from letsencrypt.acme import jose from letsencrypt.acme import messages2 +from letsencrypt.client import errors + class Network(object): """ACME networking. @@ -47,7 +49,9 @@ class Network(object): uri=response.headers['location'], new_authz_uri=response.links['next']['url'], terms_of_service=terms_of_service) - assert regr.body.key == self.key.public() + + if regr.body.key != self.key.public() or regr.body.contact != contact: + raise errors.UnexpectedUpdate(regr) return regr From 227d947d4c586b5b41b5b1474ecf471307b3742e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 17:08:37 +0000 Subject: [PATCH 16/92] Update network2 docs --- letsencrypt/client/network2.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 5755d25d3..c5e9a7b80 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -37,6 +37,14 @@ class Network(object): return response def register(self, contact=messages2.Registration._fields['contact'].default): + """Register. + + :returns: Registration Resource. + :rtype: `.RegistrationResource` + + :raises letsencrypt.client.errors.UnexpectedUpdate: + + """ new_reg = messages2.Registration(contact=contact) response = self._post(self.new_reg_uri, self._wrap_in_jws(new_reg)) @@ -58,10 +66,10 @@ class Network(object): def update_registration(self, regr): """Update registration. - :pram regr: Registration resource. + :pram regr: Registration Resource. :type regr: `.RegistrationResource` - :returns: Updated registration resource. + :returns: Updated Registration Resource. :rtype: `.RegistrationResource` """ @@ -176,7 +184,7 @@ class Network(object): def check_cert(self, certr): """Check for new cert. - :param certr: CertificateResource + :param certr: Certificate Resource :type certr: `.CertificateResource` """ From b24487a14b5f557bc003d1f311519d516c18caa5 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 17:40:20 +0000 Subject: [PATCH 17/92] restified example: NEW_REG_URL only --- examples/restified.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/restified.py b/examples/restified.py index a769233e8..7947887eb 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -11,11 +11,7 @@ from letsencrypt.client import network2 logger = logging.getLogger() logger.setLevel(logging.DEBUG) -URL_ROOT = 'https://www.letsencrypt-demo.org' -NEW_REG_URL = URL_ROOT + '/acme/new-reg' -NEW_AUTHZ_URL = URL_ROOT + '/acme/new-authz' -#NEW_CERT_URL = URL_ROOT + '/acme/new-certz' - +NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg' key = jose.JWKRSA.load(pkg_resources.resource_string( 'letsencrypt.acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) From 5c40daaf1cb25895c44a06e733456af77ab75f73 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 18:32:58 +0000 Subject: [PATCH 18/92] ImmutableMap.update --- letsencrypt/acme/jose/util.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/letsencrypt/acme/jose/util.py b/letsencrypt/acme/jose/util.py index 5f516884f..e8d2a17a6 100644 --- a/letsencrypt/acme/jose/util.py +++ b/letsencrypt/acme/jose/util.py @@ -57,6 +57,12 @@ class ImmutableMap(collections.Mapping, collections.Hashable): for slot in self.__slots__: object.__setattr__(self, slot, kwargs.pop(slot)) + def update(self, **kwargs): + """Return updated map.""" + items = dict(self) + items.update(kwargs) + return type(self)(**items) + def __getitem__(self, key): try: return getattr(self, key) From 9832e5c6d65a6f9f9f64d733bd17ce37930a1893 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 18:30:37 +0000 Subject: [PATCH 19/92] network2: update_registration --- letsencrypt/client/network2.py | 40 +++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index c5e9a7b80..eb58ce103 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -36,6 +36,23 @@ class Network(object): logging.debug('Received response %s: %s', response, response.text) return response + def _regr_from_response(self, response, uri=None, new_authz_uri=None): + terms_of_service = ( + response.links['next']['url'] + if 'terms-of-service' in response.links else None) + + if new_authz_uri is None: + try: + new_authz_uri = response.links['next']['url'] + except KeyError: + raise errors.NetworkError('"next" link missing') + + return messages2.RegistrationResource( + body=messages2.Registration.from_json(response.json()), + uri=response.headers.get('location', uri), + new_authz_uri=new_authz_uri, + terms_of_service=terms_of_service) + def register(self, contact=messages2.Registration._fields['contact'].default): """Register. @@ -50,14 +67,7 @@ class Network(object): response = self._post(self.new_reg_uri, self._wrap_in_jws(new_reg)) assert response.status_code == httplib.CREATED # TODO: handle errors - terms_of_service = (response.links['next']['url'] - if 'terms-of-service' in response.links else None) - regr = messages2.RegistrationResource( - body=messages2.Registration.from_json(response.json()), - uri=response.headers['location'], - new_authz_uri=response.links['next']['url'], - terms_of_service=terms_of_service) - + regr = self._regr_from_response(response) if regr.body.key != self.key.public() or regr.body.contact != contact: raise errors.UnexpectedUpdate(regr) @@ -75,6 +85,20 @@ class Network(object): """ response = self._post(regr.uri, self._wrap_in_jws(regr.body)) + # TODO: Boulder returns httplib.ACCEPTED + #assert response.status_code == httplib.OK + + # TODO: Boulder does not set Location or Link on update + # (c.f. acme-spec #94) + + updated_regr = self._regr_from_response( + response, uri=regr.uri, new_authz_uri=regr.new_authz_uri) + if updated_regr != regr: + pass + # TODO: Boulder reregisters with new recoveryToken and new URI + #raise errors.UnexpectedUpdate(regr) + return updated_regr + def request_challenges(self, identifier, regr): """Request challenges. From 2fb3bd8728cf87b0f4a658391ce1a4a3ba13e465 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 18:32:44 +0000 Subject: [PATCH 20/92] UnexpectedUpdate in Network.answer_challenge --- letsencrypt/client/network2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index eb58ce103..d30c922da 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -133,9 +133,12 @@ class Network(object): :returns: Updated challenge resource. :rtype: `.ChallengeResource` + :raises errors.UnexpectedUpdate: + """ response = self._post(challr.uri, self._wrap_in_jws(response)) - assert response.headers['location'] == challr.uri + if response.headers['location'] != challr.uri: + raise UnexpectedUpdate(response.headers['location']) updated_challr = messages2.ChallengeResource( body=challenges.Challenge.from_json(response.json()), uri=challr.uri) From 7e5ccddf7eb9d817ba7dacaf3ac21baea781abaf Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 18:33:29 +0000 Subject: [PATCH 21/92] restified example: auto-accept TOS --- examples/restified.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/restified.py b/examples/restified.py index 7947887eb..1a11bf783 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -19,6 +19,9 @@ net = network2.Network(NEW_REG_URL, key) regr = net.register(contact=( 'mailto:cert-admin@example.com', 'tel:+12025551212')) +logging.info('Auto-accepting TOS: %s', regr.terms_of_service) +net.update_registration(regr.update( + body=regr.body.update(agreement=regr.terms_of_service))) logging.debug(regr) authzr = net.request_challenges( From c242091b4ebfc4aba51b29ddd9794369e855eb3a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 18:39:05 +0000 Subject: [PATCH 22/92] UnexpectedUpdate in Network.request_challenges --- letsencrypt/client/network2.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index d30c922da..d927ecede 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -116,7 +116,9 @@ class Network(object): body=messages2.Authorization.from_json(response.json()), uri=response.headers['location'], new_cert_uri=response.links['next']['url']) - assert authzr.body.key == self.key.public() + if (authzr.body.key != self.key.public() + or authzr.body.identifier != identifier): + raise errors.UnexpectedUpdate(authzr) return authzr # TODO: anything below is also stub, bot not working, not tested at all From d9176d426727e6c2fc79fd843a7a3377473ae5e2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 18:50:53 +0000 Subject: [PATCH 23/92] Improve request_issuance --- examples/restified.py | 9 +++++++-- letsencrypt/client/network2.py | 7 +++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/restified.py b/examples/restified.py index 1a11bf783..1428c96cc 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -2,6 +2,8 @@ import logging import os import pkg_resources +import M2Crypto + from letsencrypt.acme import messages2 from letsencrypt.acme import jose @@ -26,7 +28,10 @@ logging.debug(regr) authzr = net.request_challenges( identifier=messages2.Identifier( - typ=messages2.IdentifierFQDN, value="example1.com"), + typ=messages2.IdentifierFQDN, value='example1.com'), regr=regr) +logging.debug(authzr) -print authzr +csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string( + 'letsencrypt.client.tests', os.path.join('testdata', 'csr.pem'))) +net.request_issuance(csr, (authzr,)) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index d927ecede..b2bfb8220 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -4,6 +4,8 @@ import logging import requests +import M2Crypto + from letsencrypt.acme import jose from letsencrypt.acme import messages2 @@ -179,7 +181,8 @@ class Network(object): :param authzrs: `list` of `.AuthorizationResource` """ - req = CertificateRequest( + # TODO: assert len(authzrs) == number of SANs + req = messages2.CertificateRequest( csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) response = self._post( authzrs[0].new_cert_uri, # TODO: acme-spec #90 @@ -187,7 +190,7 @@ class Network(object): # assert content-type: application/pkix-cert return messages2.CertificateResource( authzrs=authzrs, - body=M2Crypto.X509.load_der_string(response.text), + body=M2Crypto.X509.load_cert_der_string(response.text), cert_chain_uri=response.links['up']['url']) def poll_and_request_issuance(self, csr, authzrs, mintime=5): From 3dcf81dbb65d8d2c377f30aa983540d5134450a4 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 18:55:32 +0000 Subject: [PATCH 24/92] network2: Improve error handling --- examples/restified.py | 5 ++++- letsencrypt/acme/messages2.py | 7 ++++--- letsencrypt/client/network2.py | 7 +++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/restified.py b/examples/restified.py index 1428c96cc..99d07a067 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -34,4 +34,7 @@ logging.debug(authzr) csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string( 'letsencrypt.client.tests', os.path.join('testdata', 'csr.pem'))) -net.request_issuance(csr, (authzr,)) +try: + net.request_issuance(csr, (authzr,)) +except messages2.Error as error: + print error.detail diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 37a384aa4..903746ae7 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -8,7 +8,7 @@ from letsencrypt.acme import other from letsencrypt.acme import util -class Error(jose.JSONObjectWithFields): +class Error(jose.JSONObjectWithFields, Exception): """ACME error. https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 @@ -23,10 +23,11 @@ class Error(jose.JSONObjectWithFields): "badCSR": "The CSR is unacceptable (e.g., due to a short key)", } - typ = jose.Field('type') + typ = jose.Field('type', omitempty=True) # Boulder omits, spec requires title = jose.Field('title', omitempty=True) detail = jose.Field('detail') - instance = jose.Field('instance') + # Boulder omits, spec requires + instance = jose.Field('instance', omitempty=True) @typ.encoder def typ(value): diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index b2bfb8220..3c68a17c7 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -33,9 +33,16 @@ class Network(object): payload=dumps, key=self.key, alg=self.alg).json_dumps() def _post(self, uri, data): + """Send POST data. + + :raises letsencrypt.acme.messages2.Error: + + """ logging.debug('Sending data: %s', data) response = requests.post(uri, data) logging.debug('Received response %s: %s', response, response.text) + if not response.ok: + raise messages2.Error.from_json(response.json()) return response def _regr_from_response(self, response, uri=None, new_authz_uri=None): From 3676a6d87ab0930c67bfd212c77d01c2b96ff1e3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 19:05:09 +0000 Subject: [PATCH 25/92] network2: Update poll() --- examples/restified.py | 2 ++ letsencrypt/client/network2.py | 31 +++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/examples/restified.py b/examples/restified.py index 99d07a067..b68b3b047 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -32,6 +32,8 @@ authzr = net.request_challenges( regr=regr) logging.debug(authzr) +authzr = net.poll(authzr) + csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string( 'letsencrypt.client.tests', os.path.join('testdata', 'csr.pem'))) try: diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 3c68a17c7..34419c209 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -108,6 +108,23 @@ class Network(object): #raise errors.UnexpectedUpdate(regr) return updated_regr + def _authzr_from_response(self, response, identifier, + uri=None, new_cert_uri=None): + if new_cert_uri is None: + try: + new_cert_uri = response.links['next']['url'] + except KeyError: + raise errors.NetworkError('"next" link missing') + + authzr = messages2.AuthorizationResource( + body=messages2.Authorization.from_json(response.json()), + uri=response.headers.get('location', uri), + new_cert_uri=new_cert_uri) + if (authzr.body.key != self.key.public() + or authzr.body.identifier != identifier): + raise errors.UnexpectedUpdate(authzr) + return authzr + def request_challenges(self, identifier, regr): """Request challenges. @@ -121,14 +138,7 @@ class Network(object): new_authz = messages2.Authorization(identifier=identifier) response = self._post(regr.new_authz_uri, self._wrap_in_jws(new_authz)) assert response.status_code == httplib.CREATED # TODO: handle errors - authzr = messages2.AuthorizationResource( - body=messages2.Authorization.from_json(response.json()), - uri=response.headers['location'], - new_cert_uri=response.links['next']['url']) - if (authzr.body.key != self.key.public() - or authzr.body.identifier != identifier): - raise errors.UnexpectedUpdate(authzr) - return authzr + return self._authzr_from_response(response, identifier) # TODO: anything below is also stub, bot not working, not tested at all @@ -178,6 +188,11 @@ class Network(object): :rtype: (`.AuthorizationResource`, `int`) """ + response = requests.get(authzr.uri) + updated_authzr = self._authzr_from_response( + response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri) + # TODO check UnexpectedUpdate + return updated_authzr def request_issuance(self, csr, authzrs): """Request issuance. From f29fe21dddce30cc018941cb81b830c64c3584b5 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 19:12:24 +0000 Subject: [PATCH 26/92] network2: retry-after stub --- letsencrypt/client/network2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 34419c209..13badceec 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -189,10 +189,13 @@ class Network(object): """ response = requests.get(authzr.uri) + retry_after = 0 # TODO, get it from response.headers.get('Retry-After') + updated_authzr = self._authzr_from_response( response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri) # TODO check UnexpectedUpdate - return updated_authzr + + return updated_authzr, retry_after def request_issuance(self, csr, authzrs): """Request issuance. From 0c30bcbf3e0dd17ee5984d6151b7be4ada4a02ce Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 24 Mar 2015 19:30:14 +0000 Subject: [PATCH 27/92] Fix "pool tuple" bug in restified example --- examples/restified.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/restified.py b/examples/restified.py index b68b3b047..fe8aca22f 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -32,7 +32,7 @@ authzr = net.request_challenges( regr=regr) logging.debug(authzr) -authzr = net.poll(authzr) +authzr, retry_after = net.poll(authzr) csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string( 'letsencrypt.client.tests', os.path.join('testdata', 'csr.pem'))) From 8e3a496b8b3f7be3e934d7b0ce1d87971862ab24 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 24 Mar 2015 16:21:09 -0700 Subject: [PATCH 28/92] simplify path_satisfied test --- letsencrypt/client/auth_handler.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index 41c7a9f68..05f3722cf 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -203,9 +203,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes def _path_satisfied(self, dom): """Returns whether a path has been completely satisfied.""" - return all( - self.responses[dom][i] is not None and - self.responses[dom][i] is not False for i in self.paths[dom]) + # Make sure that there are no 'None' or 'False' entries along path. + return all(self.responses[dom][i] for i in self.paths[dom]) def _get_chall_pref(self, domain): """Return list of challenge preferences. From a1fe6039d874a15b8defaf45dc40ddd8cdad0db3 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 24 Mar 2015 17:49:38 -0700 Subject: [PATCH 29/92] Fix bug with no DVSNI challenges --- letsencrypt/client/apache/dvsni.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/letsencrypt/client/apache/dvsni.py b/letsencrypt/client/apache/dvsni.py index b980fdb36..033bcde20 100644 --- a/letsencrypt/client/apache/dvsni.py +++ b/letsencrypt/client/apache/dvsni.py @@ -50,7 +50,7 @@ class ApacheDvsni(object): def perform(self): """Peform a DVSNI challenge.""" if not self.achalls: - return None + return [] # Save any changes to the configuration as a precaution # About to make temporary changes to the config self.configurator.save() @@ -160,19 +160,19 @@ class ApacheDvsni(object): ips = " ".join(str(i) for i in ip_addrs) document_root = os.path.join( self.configurator.config.config_dir, "dvsni_page/") - return ("\n" - "ServerName " + achall.nonce_domain + "\n" - "UseCanonicalName on\n" - "SSLStrictSNIVHostCheck on\n" - "\n" - "LimitRequestBody 1048576\n" - "\n" - "Include " + self.configurator.parser.loc["ssl_options"] + "\n" - "SSLCertificateFile " + self.get_cert_file(achall) + "\n" - "SSLCertificateKeyFile " + achall.key.file + "\n" - "\n" - "DocumentRoot " + document_root + "\n" - "\n\n") + return ("{0}" + "ServerName " + achall.nonce_domain + "{0}" + "UseCanonicalName on{0}" + "SSLStrictSNIVHostCheck on{0}" + "{0}" + "LimitRequestBody 1048576{0}" + "{0}" + "Include " + self.configurator.parser.loc["ssl_options"] + "{0}" + "SSLCertificateFile " + self.get_cert_file(achall) + "{0}" + "SSLCertificateKeyFile " + achall.key.file + "{0}" + "{0}" + "DocumentRoot " + document_root + "{0}" + "{0}{0}".format(os.linesep)) def get_cert_file(self, achall): """Returns standardized name for challenge certificate. From 4dfc7ea3582765fc5d04b33286b9e4b4794993a2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 05:59:38 +0000 Subject: [PATCH 30/92] network2: _get, improve netwrok error handling --- letsencrypt/client/network2.py | 48 ++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 13badceec..758c48229 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -32,17 +32,49 @@ class Network(object): return jose.JWS.sign( payload=dumps, key=self.key, alg=self.alg).json_dumps() - def _post(self, uri, data): - """Send POST data. + def _get(self, uri, **kwargs): + """Send GET request. - :raises letsencrypt.acme.messages2.Error: + :raises letsencrypt.client.errors.NetworkError: + + :returns: HTTP Response + :rtype: `requests.Response` """ - logging.debug('Sending data: %s', data) - response = requests.post(uri, data) + try: + return requests.get(uri, **kwargs) + except requests.exception.RequestException as error: + raise errors.NetworkError(error) + + def _post(self, uri, data, content_type='application/json', **kwargs): + """Send POST data. + + :param str content_type: Expected Content-Type, fails if not set. + + :raises letsencrypt.acme.messages2.NetworkError: + + :returns: HTTP Response + :rtype: `requests.Response` + + """ + logging.debug('Sending POST data: %s', data) + try: + response = requests.post(uri, data=data, **kwargs) + except requests.exception.RequestException as error: + raise errors.NetworkError(error) logging.debug('Received response %s: %s', response, response.text) + if not response.ok: - raise messages2.Error.from_json(response.json()) + if response.content_type == 'application/json': + raise messages2.Error.from_json(response.json()) + else: + raise errors.NetworkError(response) + + # TODO: Boulder messes up Content-Type #56 + #if response.headers['content-type'] != content_type: + # raise errors.NetworkError( + # 'Server returned unexpected content-type header') + return response def _regr_from_response(self, response, uri=None, new_authz_uri=None): @@ -188,7 +220,7 @@ class Network(object): :rtype: (`.AuthorizationResource`, `int`) """ - response = requests.get(authzr.uri) + response = self._get(authzr.uri) retry_after = 0 # TODO, get it from response.headers.get('Retry-After') updated_authzr = self._authzr_from_response( @@ -247,7 +279,7 @@ class Network(object): """ # TODO: acme-spec 5.1 table action should be renamed to # "refresh cert", and this method integrated with self.refresh - return requests.get(certr.uri) + return self._get(certr.uri) def refresh(self, certr): """Refresh certificate.""" From 1e45edd5485ff1fc59118f6d12c88971da1f1c2d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 05:59:56 +0000 Subject: [PATCH 31/92] Add docstring --- letsencrypt/client/network2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 758c48229..7e45c3b59 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -27,6 +27,7 @@ class Network(object): self.alg = alg def _wrap_in_jws(self, data): + """Wrap `JSONDeSerializable` object in JWS.""" dumps = data.json_dumps() logging.debug('Serialized JSON: %s', dumps) return jose.JWS.sign( From 66bc89f18648b9981e089887cd98baf717645f2a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 06:03:16 +0000 Subject: [PATCH 32/92] Boulder messaes up Content-Type --- letsencrypt/client/network2.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 7e45c3b59..b565bed8c 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -66,10 +66,11 @@ class Network(object): logging.debug('Received response %s: %s', response, response.text) if not response.ok: - if response.content_type == 'application/json': - raise messages2.Error.from_json(response.json()) - else: - raise errors.NetworkError(response) + # Boulder messes up Content-Type #56 + #if response.headers['content-type'] == 'application/json': + raise messages2.Error.from_json(response.json()) + #else: + # raise errors.NetworkError(response) # TODO: Boulder messes up Content-Type #56 #if response.headers['content-type'] != content_type: From 3786170a89762dafa3c7dbb4d55bfcfc248e3592 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 06:10:38 +0000 Subject: [PATCH 33/92] request_issuance Accept and Content-Type --- letsencrypt/client/network2.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index b565bed8c..00c63e18c 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -243,10 +243,14 @@ class Network(object): # TODO: assert len(authzrs) == number of SANs req = messages2.CertificateRequest( csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) + + content_type = 'application/plix-cert' # TODO: add 'cert_type 'argument response = self._post( authzrs[0].new_cert_uri, # TODO: acme-spec #90 - self._wrap_in_jws(req)) - # assert content-type: application/pkix-cert + self._wrap_in_jws(req), + content_type=content_type, + headers={'Accept': content_type}) + return messages2.CertificateResource( authzrs=authzrs, body=M2Crypto.X509.load_cert_der_string(response.text), From a4704d72bd1d5994300169317dd6d781eb9ce3a1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 07:41:36 +0000 Subject: [PATCH 34/92] network2: _check_content_typ --- letsencrypt/client/network2.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 00c63e18c..8fd37df51 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -33,7 +33,14 @@ class Network(object): return jose.JWS.sign( payload=dumps, key=self.key, alg=self.alg).json_dumps() - def _get(self, uri, **kwargs): + def _check_content_type(self, response, content_type): + # TODO: Boulder messes up Content-Type #56 + #if response.headers['content-type'] != content_type: + # raise errors.NetworkError( + # 'Server returned unexpected content-type header') + pass + + def _get(self, uri, content_type='application/json', **kwargs): """Send GET request. :raises letsencrypt.client.errors.NetworkError: @@ -43,9 +50,11 @@ class Network(object): """ try: - return requests.get(uri, **kwargs) + response = requests.get(uri, **kwargs) except requests.exception.RequestException as error: raise errors.NetworkError(error) + self._check_content_type(response, content_type) + return response def _post(self, uri, data, content_type='application/json', **kwargs): """Send POST data. @@ -72,11 +81,7 @@ class Network(object): #else: # raise errors.NetworkError(response) - # TODO: Boulder messes up Content-Type #56 - #if response.headers['content-type'] != content_type: - # raise errors.NetworkError( - # 'Server returned unexpected content-type header') - + self._check_content_type(response, content_type) return response def _regr_from_response(self, response, uri=None, new_authz_uri=None): From 9c8a6f7b045d523d7a2014b247fae69142a9b230 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 07:42:21 +0000 Subject: [PATCH 35/92] network2: use ImmutableMap.update() --- letsencrypt/client/network2.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 8fd37df51..3a1057c43 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -199,9 +199,8 @@ class Network(object): response = self._post(challr.uri, self._wrap_in_jws(response)) if response.headers['location'] != challr.uri: raise UnexpectedUpdate(response.headers['location']) - updated_challr = messages2.ChallengeResource( - body=challenges.Challenge.from_json(response.json()), - uri=challr.uri) + updated_challr = challr.update( + body=challenges.Challenge.from_json(response.json())) return updated_challr def answer_challenges(self, challrs, responses): From eeb4f632bf846501e9d36f438c9e8cc5f20cb6ae Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 07:42:36 +0000 Subject: [PATCH 36/92] network2: _get_cert, fetch_chain --- letsencrypt/client/network2.py | 39 ++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 3a1057c43..0128fbf83 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -21,6 +21,8 @@ class Network(object): """ + DER_CONTENT_TYPE = 'application/plix-cert' + def __init__(self, new_reg_uri, key, alg=jose.RS256): self.new_reg_uri = new_reg_uri self.key = key @@ -248,7 +250,7 @@ class Network(object): req = messages2.CertificateRequest( csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) - content_type = 'application/plix-cert' # TODO: add 'cert_type 'argument + content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument response = self._post( authzrs[0].new_cert_uri, # TODO: acme-spec #90 self._wrap_in_jws(req), @@ -280,23 +282,52 @@ class Network(object): return request_issuance(csr, authzrs) + def _get_cert(self, uri): + content_type = self.DER_CONTENT_TYPE # TODO: make it a param + response = self._get(uri, headers={'Accept': content_type}, + content_type=content_type) + return response, M2Crypto.X509.load_cert_der_string(response.text) + def check_cert(self, certr): """Check for new cert. :param certr: Certificate Resource :type certr: `.CertificateResource` + :returns: Updated Certificate Resource. + :rtype: `.CertificateResource` + """ # TODO: acme-spec 5.1 table action should be renamed to # "refresh cert", and this method integrated with self.refresh - return self._get(certr.uri) + response, cert = self._get_cert(certr.uri) + if not response.headers['location'] != certr.uri: + raise UnexpectedUpdate(response.text) + return certr.update(body=cert) def refresh(self, certr): - """Refresh certificate.""" + """Refresh certificate. + + :param certr: Certificate Resource + :type certr: `.CertificateResource` + + :returns: Updated Certificate Resource. + :rtype: `.CertificateResource` + + """ return self.check_cert(certr) def fetch_chain(self, certr): - """Fetch chain for certificate.""" + """Fetch chain for certificate. + + :param certr: Certificate Resource + :type certr: `.CertificateResource` + + :returns: Certificate chain + :rtype: `M2Crypto.X509.X509` + + """ + return self._get_cert(certr.cert_chain_uri) def revoke(self, certr, when='now'): """Revoke certificate. From 9b33c9a6857da6303708dea8fb8c45130d0afb56 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 07:54:08 +0000 Subject: [PATCH 37/92] network2: revoke --- letsencrypt/client/network2.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 0128fbf83..d78e5b78d 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -338,3 +338,7 @@ class Network(object): """ rev = messages2.Revocation(revoke=when, authorizations=tuple( authzr.uri for authzr in certr.authzrs)) + response = self._post(certr.uri, self._wrap_in_jws(rev)) + if response.status_code != httplib.OK: + raise errors.NetworkError( + 'Successful revocation must return HTTP OK status') From f5a6bb389ec5a2fb1806e15a0a365133282f6cb8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 10:21:01 +0000 Subject: [PATCH 38/92] Fix #316 --- letsencrypt/client/apache/dvsni.py | 40 +++++++++++++------ letsencrypt/client/tests/apache/dvsni_test.py | 2 +- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/letsencrypt/client/apache/dvsni.py b/letsencrypt/client/apache/dvsni.py index 033bcde20..29ae57308 100644 --- a/letsencrypt/client/apache/dvsni.py +++ b/letsencrypt/client/apache/dvsni.py @@ -26,6 +26,23 @@ class ApacheDvsni(object): :param str challenge_conf: location of the challenge config file """ + + VHOST_TEMPLATE = """\ + + ServerName {server_name} + UseCanonicalName on + SSLStrictSNIVHostCheck on + + LimitRequestBody 1048576 + + Include {ssl_options_conf_path} + SSLCertificateFile {cert_path} + SSLCertificateKeyFile {key_path} + + DocumentRoot {document_root} + + +""" def __init__(self, configurator): self.configurator = configurator self.achalls = [] @@ -160,19 +177,16 @@ class ApacheDvsni(object): ips = " ".join(str(i) for i in ip_addrs) document_root = os.path.join( self.configurator.config.config_dir, "dvsni_page/") - return ("{0}" - "ServerName " + achall.nonce_domain + "{0}" - "UseCanonicalName on{0}" - "SSLStrictSNIVHostCheck on{0}" - "{0}" - "LimitRequestBody 1048576{0}" - "{0}" - "Include " + self.configurator.parser.loc["ssl_options"] + "{0}" - "SSLCertificateFile " + self.get_cert_file(achall) + "{0}" - "SSLCertificateKeyFile " + achall.key.file + "{0}" - "{0}" - "DocumentRoot " + document_root + "{0}" - "{0}{0}".format(os.linesep)) + # TODO: Python docs is not clear how mutliline string literal + # newlines are parsed on different platforms. At least on + # Linux (Debian sid), when source file uses CLRF, Python still + # parses it as '\n'... c.f.: + # https://docs.python.org/2.7/reference/lexical_analysis.html + return self.VHOST_TEMPLATE.format( + vhost=ips, server_name=achall.nonce_domain, + ssl_options_conf_path=self.configurator.parser.loc["ssl_options"], + cert_path=self.get_cert_file(achall), key_path=achall.key.file, + document_root=document_root).replace('\n', os.linesep) def get_cert_file(self, achall): """Returns standardized name for challenge certificate. diff --git a/letsencrypt/client/tests/apache/dvsni_test.py b/letsencrypt/client/tests/apache/dvsni_test.py index 384e426bb..110916e94 100644 --- a/letsencrypt/client/tests/apache/dvsni_test.py +++ b/letsencrypt/client/tests/apache/dvsni_test.py @@ -60,7 +60,7 @@ class DvsniPerformTest(util.ApacheTest): def test_perform0(self): resp = self.sni.perform() - self.assertTrue(resp is None) + self.assertTrue(len(resp) == 0) def test_setup_challenge_cert(self): # This is a helper function that can be used for handling From 23e92da0b5873b6469f8f4ef58421d28d2a2af2f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 10:25:27 +0000 Subject: [PATCH 39/92] Fix typo --- letsencrypt/client/apache/dvsni.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client/apache/dvsni.py b/letsencrypt/client/apache/dvsni.py index 29ae57308..71bd03c7e 100644 --- a/letsencrypt/client/apache/dvsni.py +++ b/letsencrypt/client/apache/dvsni.py @@ -179,7 +179,7 @@ class ApacheDvsni(object): self.configurator.config.config_dir, "dvsni_page/") # TODO: Python docs is not clear how mutliline string literal # newlines are parsed on different platforms. At least on - # Linux (Debian sid), when source file uses CLRF, Python still + # Linux (Debian sid), when source file uses CRLF, Python still # parses it as '\n'... c.f.: # https://docs.python.org/2.7/reference/lexical_analysis.html return self.VHOST_TEMPLATE.format( From e77d9026e10072e6251ce5e59c1805140cb8e1dc Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 11:13:26 +0000 Subject: [PATCH 40/92] Update network2 docs --- letsencrypt/acme/messages2.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 903746ae7..2d8514183 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -122,7 +122,7 @@ class RegistrationResource(Resource): class Registration(ResourceBody): - """Registration resource.""" + """Registration resource body.""" # on new-reg key server ignores 'key' and populates it based on # JWS.signature.combined.jwk @@ -169,26 +169,24 @@ class AuthorizationResource(Resource): class Authorization(ResourceBody): + """Authorization resource body.""" identifier = jose.Field('identifier', decoder=Identifier.from_json) - - # acme-spec marks 'key' as 'required', but new-authz does not need - # to carry it, server will take 'key' from the 'jwk' found in the - # JWS - key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) - status = jose.Field('status', omitempty=True, decoder=Status.from_json) challenges = jose.Field('challenges', omitempty=True) - - # TODO: 'expires' is allowed for Authorization Resources in - # general, but for Authorization '[t]he "expires" field MUST be - # absent'... then acme-spec gives example with 'expires' - # present... That's confusing! - expires = jose.Field('expires', omitempty=True) # TODO: this is date - combinations = jose.Field('combinations', omitempty=True) - # TODO: 'The client MAY provide contact information in the - # "contact" field in this or any subsequent request.' ??? + # TODO: acme-spec #92, #98 + key = Registration._fields['key'] + contact = Registration._fields['contact'] + + # TODO: move status/expires to AuthorizationResource for symmetry + # with ChallengeResource.status/validated? + status = jose.Field('status', omitempty=True, decoder=Status.from_json) + # TODO: 'expires' is allowed for Authorization Resources in + # general, but for Key Authorization '[t]he "expires" field MUST + # be absent'... then acme-spec gives example with 'expires' + # present... That's confusing! + expires = jose.Field('expires', omitempty=True) # TODO: this is date @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument From 920152bb177df0fdfc73b03786cffd786a2589ed Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 11:22:50 +0000 Subject: [PATCH 41/92] messages2.Challenge --- letsencrypt/acme/messages2.py | 37 ++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 2d8514183..958923b93 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -88,6 +88,7 @@ class IdentifierType(_Constant): POSSIBLE_NAMES = {} IdentifierFQDN = IdentifierType('dns') # IdentifierDNS in Boulder + class Identifier(jose.JSONObjectWithFields): """ACME identifier.""" typ = jose.Field('type', decoder=IdentifierType.from_json) @@ -139,22 +140,36 @@ class ChallengeResource(Resource, jose.JSONObjectWithFields): :ivar authz_uri: URI found in the 'up' Link header. """ - __slots__ = ('body',)# 'authz_uri') + __slots__ = ('body', 'authz_uri') + +class Challenge(ResourceBody): + """Challenge resource body. + + .. todo:: + Confusingly, this has the same name as + `challenges.Challenge`. Indeed, this class could be integrated + with challenges.Challenge, but this way it would be confusing + when compared to acme-spec, where all challenges are presented + without 'uri', 'status', or 'validated' fields. + + """ + + __slots__ = ('chall',) uri = jose.Field('uri') status = jose.Field('status', decoder=Status.from_json) # TODO: de/encode datetime validated = jose.Field('validated', omitempty=True) def to_json(self): - jobj = super(ChallengeResource, self).to_json() - jobj.update(self.body.to_json()) + jobj = super(Challenge, self).to_json() + jobj.update(self.chall.to_json()) return jobj @classmethod def fields_from_json(cls, jobj): - fields = super(ChallengeResource, cls).fields_from_json(jobj) - fields['body'] = challenges.Challenge.from_json(jobj) + fields = super(Challenge, cls).fields_from_json(jobj) + fields['chall'] = challenges.Challenge.from_json(jobj) return fields @@ -169,7 +184,11 @@ class AuthorizationResource(Resource): class Authorization(ResourceBody): - """Authorization resource body.""" + """Authorization resource body. + + :ivar challenges: `list` of `Challenge` + + """ identifier = jose.Field('identifier', decoder=Identifier.from_json) challenges = jose.Field('challenges', omitempty=True) @@ -179,8 +198,6 @@ class Authorization(ResourceBody): key = Registration._fields['key'] contact = Registration._fields['contact'] - # TODO: move status/expires to AuthorizationResource for symmetry - # with ChallengeResource.status/validated? status = jose.Field('status', omitempty=True, decoder=Status.from_json) # TODO: 'expires' is allowed for Authorization Resources in # general, but for Key Authorization '[t]he "expires" field MUST @@ -190,7 +207,9 @@ class Authorization(ResourceBody): @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(ChallengeResource.from_json(chall) for chall in value) + return tuple( + ChallengeResource(body=Challenge.from_json(chall), authz_uri=None) + for chall in value) @property def resolved_combinations(self): From 4eef08911aebb430dba7afa22edfbfcb1dd2ccc5 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 12:44:37 +0000 Subject: [PATCH 42/92] network2: priority queue polling, _retry_after --- examples/restified.py | 2 +- letsencrypt/client/network2.py | 62 ++++++++++++++++++++++++---------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/examples/restified.py b/examples/restified.py index fe8aca22f..6ae103ce0 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -32,7 +32,7 @@ authzr = net.request_challenges( regr=regr) logging.debug(authzr) -authzr, retry_after = net.poll(authzr) +authzr, authzr_response = net.poll(authzr) csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string( 'letsencrypt.client.tests', os.path.join('testdata', 'csr.pem'))) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index d78e5b78d..e9fb53d6c 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -1,8 +1,12 @@ """Networking for ACME protocol v02.""" +import datetime +import heapq import httplib import logging +import time import requests +import werkzeug import M2Crypto @@ -216,26 +220,32 @@ class Network(object): return [self.answer_challenge(challr, response) for challr, response in itertools.izip(challrs, responses)] + def _retry_after(self, response, mintime): + ra = response.headers.get('Retry-After', str(mintime)) + try: + seconds = int(ra) + except ValueError: + return werkzeug.parse_date(ra) + else: + return datetime.datetime.now() + datetime.timedelta(seconds=seconds) + def poll(self, authzr): """Poll Authorization Resource for status. :param authzr: Authorization Resource :type authzr: `.AuthorizationResource` - :returns: Updated Authorization Resource and 'Retry-After' - value (0, if such header not provided). + :returns: Updated Authorization Resource and HTTP response. - :rtype: (`.AuthorizationResource`, `int`) + :rtype: (`.AuthorizationResource`, `requests.Response`) """ response = self._get(authzr.uri) - retry_after = 0 # TODO, get it from response.headers.get('Retry-After') - updated_authzr = self._authzr_from_response( response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri) # TODO check UnexpectedUpdate - return updated_authzr, retry_after + return updated_authzr, response def request_issuance(self, csr, authzrs): """Request issuance. @@ -265,22 +275,40 @@ class Network(object): def poll_and_request_issuance(self, csr, authzrs, mintime=5): """Poll and request issuance. - :param int mintime: Minimum time before next attempt + :param int mintime: Minimum time before next attempt. + + .. todo:: add `max_attempts` or `timeout` """ - waiting = set() - finished = set() + # priority queue with datetime (based od Retry-After) as key, + # and original Authorization Resource as value + waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs] + # mapping between original Authorization Resource and the most + # recently updated one + updated = dict((authzr, authzr) for authzr in authzrs) while waiting: - authzr = waiting.pop() - updated_authzr, retry_after = self.poll(authzr) - if updated_authzr.body.status == messages2.StatusValidated: - finished.add(updated_authzr) - else: - waiting.add(updated_authzr) - # TODO: implement reasonable sleeping! + # find the smallest Retry-After, and sleep if necessary + when, authzr = heapq.heappop(waiting) + now = datetime.datetime.now() + if when > now: + seconds = (when - now).seconds + logging.debug('Sleeping for %d seconds', seconds) + time.sleep(seconds) - return request_issuance(csr, authzrs) + updated_authzr, response = self.poll(authzr) + updated[authzr] = updated_authzr + # URI must not change throughout, as we are polling + # original Authorization Resource URI only + assert updated_authzr.uri == authzr + + if updated_authzr.body.status != messages2.StatusValidated: + # push back to the priority queue, with updated retry_after + heapq.heappush(waiting, (self._retry_after( + response, mintime=mintime), authzr)) + + return request_issuance(csr, authzrs), tuple( + updated[authzr] for authzr in authzrs) def _get_cert(self, uri): content_type = self.DER_CONTENT_TYPE # TODO: make it a param From a204574b027c8640c0bfd507e5b431816ed6925d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 12:50:21 +0000 Subject: [PATCH 43/92] network2 is not so much stub anymore --- letsencrypt/client/network2.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index e9fb53d6c..610088972 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -185,8 +185,6 @@ class Network(object): assert response.status_code == httplib.CREATED # TODO: handle errors return self._authzr_from_response(response, identifier) - # TODO: anything below is also stub, bot not working, not tested at all - def answer_challenge(self, challr, response): """Answer challenge. From 073dea2624b299fab0c95e768c510432ee855483 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 12:50:51 +0000 Subject: [PATCH 44/92] Add werkzeug dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index fac3eef90..91e17b337 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ install_requires = [ 'python-augeas', 'python2-pythondialog', 'requests', + 'werkzeug', 'zope.component', 'zope.interface', # order of items in install_requires DOES matter and M2Crypto has From 0b557a0b8c2934dd408643927c222247e81a9183 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 15:22:56 +0000 Subject: [PATCH 45/92] acme-spec #88 fixed --- letsencrypt/acme/messages2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 958923b93..4b6ca98e5 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -74,7 +74,6 @@ class _Constant(jose.JSONDeSerializable): class Status(_Constant): """ACME "status" field.""" POSSIBLE_NAMES = {} -# TODO: acme-spec #88 StatusUnknown = Status('unknown') StatusPending = Status('pending') StatusProcessing = Status('processing') From 34466f745b5e044fe7df31fe27448bfc173d1904 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 16:01:32 +0000 Subject: [PATCH 46/92] RFC3339DateField (pyrfc3339) --- letsencrypt/acme/fields.py | 19 +++++++++++++++++++ letsencrypt/acme/messages2.py | 25 +++++++++++++++++++------ letsencrypt/client/network2.py | 2 +- setup.py | 1 + 4 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 letsencrypt/acme/fields.py diff --git a/letsencrypt/acme/fields.py b/letsencrypt/acme/fields.py new file mode 100644 index 000000000..020f02bd3 --- /dev/null +++ b/letsencrypt/acme/fields.py @@ -0,0 +1,19 @@ +"""ACME JSON fields.""" +import pyrfc3339 + +from letsencrypt.acme import jose + + +class RFC3339Field(jose.Field): + """RFC3339 field encoder/decoder""" + + @classmethod + def default_encoder(self, value): + return pyrfc3339.generate(value) + + @classmethod + def default_decoder(cls, value): + try: + return pyrfc3339.parse(value) + except ValueError as error: + raise jose.DeserializationError(error) diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 4b6ca98e5..8fe72e8fa 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -3,6 +3,7 @@ import jsonschema from letsencrypt.acme import challenges from letsencrypt.acme import errors +from letsencrypt.acme import fields from letsencrypt.acme import jose from letsencrypt.acme import other from letsencrypt.acme import util @@ -157,8 +158,7 @@ class Challenge(ResourceBody): __slots__ = ('chall',) uri = jose.Field('uri') status = jose.Field('status', decoder=Status.from_json) - # TODO: de/encode datetime - validated = jose.Field('validated', omitempty=True) + validated = fields.RFC3339Field('validated', omitempty=True) def to_json(self): jobj = super(Challenge, self).to_json() @@ -202,7 +202,7 @@ class Authorization(ResourceBody): # general, but for Key Authorization '[t]he "expires" field MUST # be absent'... then acme-spec gives example with 'expires' # present... That's confusing! - expires = jose.Field('expires', omitempty=True) # TODO: this is date + expires = fields.RFC3339Field('expires', omitempty=True) @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument @@ -241,8 +241,21 @@ class CertificateResource(Resource): class Revocation(jose.JSONObjectWithFields): """Revocation message.""" - class When(object): # TODO: 'now' or datetime - pass + NOW = 'now' - revoke = jose.Field('revoke') # TODO: use When + revoke = jose.Field('revoke') authorizations = CertificateRequest._fields['authorizations'] + + @revoke.decoder + def revoke(value): + if jobj == NOW: + return jobj + else: + return RFC3339Field.default_decoder(value) + + @revoke.encoder + def revoke(value): + if jobj == NOW: + return value + else: + return RFC3339Field.default_encoder(value) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 610088972..e8beb7ee4 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -355,7 +355,7 @@ class Network(object): """ return self._get_cert(certr.cert_chain_uri) - def revoke(self, certr, when='now'): + def revoke(self, certr, when=messages2.Revocation.NOW): """Revoke certificate. :param when: When should the revocation take place. diff --git a/setup.py b/setup.py index 91e17b337..b25b7fdb4 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ install_requires = [ 'pyasn1', # urllib3 InsecurePlatformWarning (#304) 'pycrypto', 'PyOpenSSL', + 'pyrfc3339', 'python-augeas', 'python2-pythondialog', 'requests', From 7d834a0ae8b68138ac18bf92b54f33074a13869f Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 25 Mar 2015 10:46:22 -0700 Subject: [PATCH 47/92] assertTrue to assertEqual --- letsencrypt/client/tests/apache/dvsni_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client/tests/apache/dvsni_test.py b/letsencrypt/client/tests/apache/dvsni_test.py index 110916e94..f3e0e9ce5 100644 --- a/letsencrypt/client/tests/apache/dvsni_test.py +++ b/letsencrypt/client/tests/apache/dvsni_test.py @@ -60,7 +60,7 @@ class DvsniPerformTest(util.ApacheTest): def test_perform0(self): resp = self.sni.perform() - self.assertTrue(len(resp) == 0) + self.assertEqual(len(resp), 0) def test_setup_challenge_cert(self): # This is a helper function that can be used for handling From 761994a5f83f123a01b5cec279c9c5e15571ed43 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 25 Mar 2015 18:37:55 +0000 Subject: [PATCH 48/92] ChallengeResource.uri --- letsencrypt/acme/messages2.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 8fe72e8fa..5aa5a84f2 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -142,6 +142,10 @@ class ChallengeResource(Resource, jose.JSONObjectWithFields): """ __slots__ = ('body', 'authz_uri') + @property + def uri(self): + return body.uri + class Challenge(ResourceBody): """Challenge resource body. From 8a9bd1ee0b58e0c54b0055cee20ecdac5cf0289f Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 25 Mar 2015 18:38:13 -0700 Subject: [PATCH 49/92] update/fix documentation of reverter --- letsencrypt/client/reverter.py | 36 ++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/letsencrypt/client/reverter.py b/letsencrypt/client/reverter.py index 715b44f80..ebb85a954 100644 --- a/letsencrypt/client/reverter.py +++ b/letsencrypt/client/reverter.py @@ -83,7 +83,8 @@ class Reverter(object): def view_config_changes(self): """Displays all saved checkpoints. - All checkpoints are printed to the console. + All checkpoints are printed by + :meth:`letsencrypt.client.interfaces.IDisplay.notification`. .. todo:: Decide on a policy for error handling, OSError IOError... @@ -130,17 +131,17 @@ class Reverter(object): os.linesep.join(output), display_util.HEIGHT) def add_to_temp_checkpoint(self, save_files, save_notes): - """Add files to temporary checkpoint + """Add files to temporary checkpoint. - param set save_files: set of filepaths to save - param str save_notes: notes about changes during the save + :param set save_files: set of filepaths to save + :param str save_notes: notes about changes during the save """ self._add_to_checkpoint_dir( self.config.temp_checkpoint_dir, save_files, save_notes) def add_to_checkpoint(self, save_files, save_notes): - """Add files to a permanent checkpoint + """Add files to a permanent checkpoint. :param set save_files: set of filepaths to save :param str save_notes: notes about changes during the save @@ -324,15 +325,18 @@ class Reverter(object): new_fd.close() def recovery_routine(self): - """Revert all previously modified files. + """Revert configuration to most recent finalized checkpoint. - First, any changes found in IConfig.temp_checkpoint_dir are removed, - then IN_PROGRESS changes are removed The order is important. - IN_PROGRESS is unable to add files that are already added by a TEMP - change. Thus TEMP must be rolled back first because that will be the - 'latest' occurrence of the file. + Remove all changes (temporary and permanent) that have not been + finalized. This is useful to protect against crashes and other + execution interruptions. """ + # First, any changes found in IConfig.temp_checkpoint_dir are removed, + # then IN_PROGRESS changes are removed The order is important. + # IN_PROGRESS is unable to add files that are already added by a TEMP + # change. Thus TEMP must be rolled back first because that will be the + # 'latest' occurrence of the file. self.revert_temporary_config() if os.path.isdir(self.config.in_progress_dir): try: @@ -385,11 +389,10 @@ class Reverter(object): return True def finalize_checkpoint(self, title): - """Move IN_PROGRESS checkpoint to timestamped checkpoint. + """Finalize the checkpoint. - Adds title to self.config.in_progress_dir CHANGES_SINCE - Move self.config.in_progress_dir to Backups directory and - rename the directory as a timestamp + Timestamps and permanently saves all changes made through the use + of :func:`~add_to_checkpoint` and :func:`~register_file_creation` :param str title: Title describing checkpoint @@ -397,6 +400,9 @@ class Reverter(object): checkpoint is not able to be finalized. """ + # Adds title to self.config.in_progress_dir CHANGES_SINCE + # Move self.config.in_progress_dir to Backups directory and + # rename the directory as a timestamp # Check to make sure an "in progress" directory exists if not os.path.isdir(self.config.in_progress_dir): return From 1c964c865bdeb9d36746c87043742d406ab56de6 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 06:46:31 +0000 Subject: [PATCH 50/92] network2: InsecurePlatformWarning fix --- letsencrypt/client/network2.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index e8beb7ee4..b8999d1ba 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -16,6 +16,10 @@ from letsencrypt.acme import messages2 from letsencrypt.client import errors +# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning +requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() + + class Network(object): """ACME networking. From d128e42f76ef41140d163e0cbaef97a76f86b2b9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 06:50:13 +0000 Subject: [PATCH 51/92] API docs for messages2/network2 --- docs/api/acme/index.rst | 9 +++++++++ docs/api/client/network2.rst | 5 +++++ 2 files changed, 14 insertions(+) create mode 100644 docs/api/client/network2.rst diff --git a/docs/api/acme/index.rst b/docs/api/acme/index.rst index 89801611e..3f4a8f6ea 100644 --- a/docs/api/acme/index.rst +++ b/docs/api/acme/index.rst @@ -8,9 +8,18 @@ Messages -------- +v00 +~~~ + .. automodule:: letsencrypt.acme.messages :members: +v02 +~~~ + +.. automodule:: letsencrypt.acme.messages2 + :members: + Challenges ---------- diff --git a/docs/api/client/network2.rst b/docs/api/client/network2.rst new file mode 100644 index 000000000..b05017551 --- /dev/null +++ b/docs/api/client/network2.rst @@ -0,0 +1,5 @@ +:mod:`letsencrypt.client.network2` +---------------------------------- + +.. automodule:: letsencrypt.client.network2 + :members: From ede635ad997aa13864aa0df48574be48da198c2b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 11:02:13 +0000 Subject: [PATCH 52/92] _check_response --- letsencrypt/client/network2.py | 63 ++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index b8999d1ba..3245dd3fb 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -30,6 +30,7 @@ class Network(object): """ DER_CONTENT_TYPE = 'application/plix-cert' + JSON_CONTENT_TYPE = 'application/json' def __init__(self, new_reg_uri, key, alg=jose.RS256): self.new_reg_uri = new_reg_uri @@ -43,14 +44,45 @@ class Network(object): return jose.JWS.sign( payload=dumps, key=self.key, alg=self.alg).json_dumps() - def _check_content_type(self, response, content_type): - # TODO: Boulder messes up Content-Type #56 - #if response.headers['content-type'] != content_type: - # raise errors.NetworkError( - # 'Server returned unexpected content-type header') - pass + @classmethod + def _check_response(cls, response, content_type=None): + """Check response content and its type. - def _get(self, uri, content_type='application/json', **kwargs): + .. note:: + Checking is not strict: skips wrong server response Content-Type + if response is an expected JSON object (c.f. Boulder #56). + + """ + response_ct = response.headers['content-type'] + + try: + # TODO: response.json() is called twice, once here, and + # once in _get and _post clients + jobj = response.json() + except ValueError as error: + jobj = None + + if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: + logging.debug( + 'Decoded JSON response, but wrong Content-Type (%s).', + response_ct) + + if not response.ok: + if jobj is not None: + try: + raise messages2.Error.from_json(jobj) + except jose.DeserializationError as error: + # Couldn't deserialize JSON object + raise errors.NetworkError((response, error)) + else: + # response is not JSON object + raise errors.NetworkError(response) + elif (content_type is not None and response_ct != content_type + and content_type != cls.JSON_CONTENT_TYPE): + raise errors.NetworkError( + 'Unexpected response Content-Type: {0}'.format(response_ct)) + + def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs): """Send GET request. :raises letsencrypt.client.errors.NetworkError: @@ -61,12 +93,12 @@ class Network(object): """ try: response = requests.get(uri, **kwargs) - except requests.exception.RequestException as error: + except requests.exceptions.RequestException as error: raise errors.NetworkError(error) - self._check_content_type(response, content_type) + self._check_response(response, content_type) return response - def _post(self, uri, data, content_type='application/json', **kwargs): + def _post(self, uri, data, content_type=JSON_CONTENT_TYPE, **kwargs): """Send POST data. :param str content_type: Expected Content-Type, fails if not set. @@ -80,18 +112,11 @@ class Network(object): logging.debug('Sending POST data: %s', data) try: response = requests.post(uri, data=data, **kwargs) - except requests.exception.RequestException as error: + except requests.exceptions.RequestException as error: raise errors.NetworkError(error) logging.debug('Received response %s: %s', response, response.text) - if not response.ok: - # Boulder messes up Content-Type #56 - #if response.headers['content-type'] == 'application/json': - raise messages2.Error.from_json(response.json()) - #else: - # raise errors.NetworkError(response) - - self._check_content_type(response, content_type) + self._check_response(response, content_type) return response def _regr_from_response(self, response, uri=None, new_authz_uri=None): From d304f538954900cbbe8e2bf23cc71853a705bf28 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 11:02:42 +0000 Subject: [PATCH 53/92] pylint network2/messages2/fields --- examples/restified.py | 2 +- letsencrypt/acme/fields.py | 2 +- letsencrypt/acme/messages2.py | 25 ++++++++++--------------- letsencrypt/client/network2.py | 25 +++++++++++++++---------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/examples/restified.py b/examples/restified.py index 6ae103ce0..651ecccd1 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -28,7 +28,7 @@ logging.debug(regr) authzr = net.request_challenges( identifier=messages2.Identifier( - typ=messages2.IdentifierFQDN, value='example1.com'), + typ=messages2.IDENTIFIER_FQDN, value='example1.com'), regr=regr) logging.debug(authzr) diff --git a/letsencrypt/acme/fields.py b/letsencrypt/acme/fields.py index 020f02bd3..59a72953b 100644 --- a/letsencrypt/acme/fields.py +++ b/letsencrypt/acme/fields.py @@ -8,7 +8,7 @@ class RFC3339Field(jose.Field): """RFC3339 field encoder/decoder""" @classmethod - def default_encoder(self, value): + def default_encoder(cls, value): return pyrfc3339.generate(value) @classmethod diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 5aa5a84f2..0fbb605d0 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -1,12 +1,7 @@ """ACME protocol v02 messages.""" -import jsonschema - from letsencrypt.acme import challenges -from letsencrypt.acme import errors from letsencrypt.acme import fields from letsencrypt.acme import jose -from letsencrypt.acme import other -from letsencrypt.acme import util class Error(jose.JSONObjectWithFields, Exception): @@ -37,7 +32,7 @@ class Error(jose.JSONObjectWithFields, Exception): @typ.decoder def typ(value): if not value.startswith(ERROR_TYPE_NAMESPACE): - raise errors.DeserializationError('Unrecognized error type') + raise jose.DeserializationError('Unrecognized error type') return value[len(ERROR_TYPE_NAMESPACE):] @@ -75,18 +70,18 @@ class _Constant(jose.JSONDeSerializable): class Status(_Constant): """ACME "status" field.""" POSSIBLE_NAMES = {} -StatusUnknown = Status('unknown') -StatusPending = Status('pending') -StatusProcessing = Status('processing') -StatusValid = Status('valid') -StatusInvalid = Status('invalid') -StatusRevoked = Status('revoked') +STATUS_UNKNOWN = Status('unknown') +STATUS_PENDING = Status('pending') +STATUS_PROCESSING = Status('processing') +STATUS_VALID = Status('valid') +STATUS_INVALID = Status('invalid') +STATUS_REVOKED = Status('revoked') class IdentifierType(_Constant): """ACME identifier type.""" POSSIBLE_NAMES = {} -IdentifierFQDN = IdentifierType('dns') # IdentifierDNS in Boulder +IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder class Identifier(jose.JSONObjectWithFields): @@ -255,11 +250,11 @@ class Revocation(jose.JSONObjectWithFields): if jobj == NOW: return jobj else: - return RFC3339Field.default_decoder(value) + return fields.RFC3339Field.default_decoder(value) @revoke.encoder def revoke(value): if jobj == NOW: return value else: - return RFC3339Field.default_encoder(value) + return fields.RFC3339Field.default_encoder(value) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 3245dd3fb..6b23e565c 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -2,6 +2,7 @@ import datetime import heapq import httplib +import itertools import logging import time @@ -10,6 +11,7 @@ import werkzeug import M2Crypto +from letsencrypt.acme import challenges from letsencrypt.acme import jose from letsencrypt.acme import messages2 @@ -119,7 +121,8 @@ class Network(object): self._check_response(response, content_type) return response - def _regr_from_response(self, response, uri=None, new_authz_uri=None): + @classmethod + def _regr_from_response(cls, response, uri=None, new_authz_uri=None): terms_of_service = ( response.links['next']['url'] if 'terms-of-service' in response.links else None) @@ -136,7 +139,8 @@ class Network(object): new_authz_uri=new_authz_uri, terms_of_service=terms_of_service) - def register(self, contact=messages2.Registration._fields['contact'].default): + def register(self, contact=messages2.Registration._fields[ + 'contact'].default): """Register. :returns: Registration Resource. @@ -231,7 +235,7 @@ class Network(object): """ response = self._post(challr.uri, self._wrap_in_jws(response)) if response.headers['location'] != challr.uri: - raise UnexpectedUpdate(response.headers['location']) + raise errors.UnexpectedUpdate(response.headers['location']) updated_challr = challr.update( body=challenges.Challenge.from_json(response.json())) return updated_challr @@ -247,12 +251,13 @@ class Network(object): return [self.answer_challenge(challr, response) for challr, response in itertools.izip(challrs, responses)] - def _retry_after(self, response, mintime): - ra = response.headers.get('Retry-After', str(mintime)) + @classmethod + def _retry_after(cls, response, mintime): + retry_after = response.headers.get('Retry-After', str(mintime)) try: - seconds = int(ra) + seconds = int(retry_after) except ValueError: - return werkzeug.parse_date(ra) + return werkzeug.parse_date(retry_after) # pylint: disable=no-member else: return datetime.datetime.now() + datetime.timedelta(seconds=seconds) @@ -329,12 +334,12 @@ class Network(object): # original Authorization Resource URI only assert updated_authzr.uri == authzr - if updated_authzr.body.status != messages2.StatusValidated: + if updated_authzr.body.status != messages2.StatusValid: # push back to the priority queue, with updated retry_after heapq.heappush(waiting, (self._retry_after( response, mintime=mintime), authzr)) - return request_issuance(csr, authzrs), tuple( + return self.request_issuance(csr, authzrs), tuple( updated[authzr] for authzr in authzrs) def _get_cert(self, uri): @@ -357,7 +362,7 @@ class Network(object): # "refresh cert", and this method integrated with self.refresh response, cert = self._get_cert(certr.uri) if not response.headers['location'] != certr.uri: - raise UnexpectedUpdate(response.text) + raise errors.UnexpectedUpdate(response.text) return certr.update(body=cert) def refresh(self, certr): From ff532469a56875ff24cc8aada7215ee2700111ab Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 13:55:23 +0000 Subject: [PATCH 54/92] Setuptools entry_points plugins --- docs/index.rst | 1 + docs/plugins.rst | 5 ++++ .../plugins/letsencrypt_example_plugins.py | 16 +++++++++++ examples/plugins/setup.py | 16 +++++++++++ letsencrypt/client/client.py | 27 +++++++++++++++++++ .../client/standalone_authenticator.py | 2 +- .../tests/standalone_authenticator_test.py | 24 ++++++++--------- letsencrypt/scripts/main.py | 8 +++--- setup.py | 6 +++++ 9 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 docs/plugins.rst create mode 100644 examples/plugins/letsencrypt_example_plugins.py create mode 100644 examples/plugins/setup.py diff --git a/docs/index.rst b/docs/index.rst index 34615168c..72be096f9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,6 +7,7 @@ Welcome to the Let's Encrypt client documentation! intro using contributing + plugins .. toctree:: :maxdepth: 1 diff --git a/docs/plugins.rst b/docs/plugins.rst new file mode 100644 index 000000000..552985aab --- /dev/null +++ b/docs/plugins.rst @@ -0,0 +1,5 @@ +======= +Plugins +======= + +You can find an example in ``examples/plugins/`` directory. diff --git a/examples/plugins/letsencrypt_example_plugins.py b/examples/plugins/letsencrypt_example_plugins.py new file mode 100644 index 000000000..6817c7f1d --- /dev/null +++ b/examples/plugins/letsencrypt_example_plugins.py @@ -0,0 +1,16 @@ +"""Example Let's Encrypt plugins.""" +import zope.interface + +from letsencrypt.client import interfaces + + +class Authenticator(object): + zope.interface.implements(interfaces.IAuthenticator) + + description = 'Example Authenticator plugin' + + def __init__(self, config): + self.config = config + + # Implement all methods from IAuthenticator, remembering to add + # "self" as first argument, e.g. def prepare(self)... diff --git a/examples/plugins/setup.py b/examples/plugins/setup.py new file mode 100644 index 000000000..845d6eb66 --- /dev/null +++ b/examples/plugins/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + + +setup( + name='letsencrypt-example-plugins', + package='letsencrypt_example_plugins.py', + install_requires=[ + 'letsencrypt', + 'zope.interface', + ], + entry_points={ + 'letsencrypt.authenticators': [ + 'example = letsencrypt_example_plugins:Authenticator', + ], + }, +) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 2f3f9a769..a448c10ce 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -1,11 +1,15 @@ """ACME protocol client class and helper functions.""" import logging import os +import pkg_resources import sys import Crypto.PublicKey.RSA import M2Crypto +import zope.interface.exceptions +import zope.interface.verify + from letsencrypt.acme import messages from letsencrypt.acme.jose import util as jose_util @@ -13,6 +17,7 @@ from letsencrypt.client import auth_handler from letsencrypt.client import client_authenticator from letsencrypt.client import crypto_util from letsencrypt.client import errors +from letsencrypt.client import interfaces from letsencrypt.client import le_util from letsencrypt.client import network from letsencrypt.client import reverter @@ -23,6 +28,28 @@ from letsencrypt.client.display import ops as display_ops from letsencrypt.client.display import enhancements +SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT = 'letsencrypt.authenticators' +"""Setuptools entry point group name for Authenticator plugins.""" + + +def init_auths(config): + """Find (setuptools entry points) and initialize Authenticators.""" + auths = {} + for entrypoint in pkg_resources.iter_entry_points( + SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT): + auth_cls = entrypoint.load() + auth = auth_cls(config) + try: + zope.interface.verify.verifyObject(interfaces.IAuthenticator, auth) + except zope.interface.exceptions.BrokenImplementation: + logging.debug( + '"%s" object does not provide IAuthenticator, skipping', + entrypoint.name) + else: + auths[auth] = entrypoint.name + return auths + + class Client(object): """ACME protocol client. diff --git a/letsencrypt/client/standalone_authenticator.py b/letsencrypt/client/standalone_authenticator.py index bf08a39ec..22597eba7 100644 --- a/letsencrypt/client/standalone_authenticator.py +++ b/letsencrypt/client/standalone_authenticator.py @@ -33,7 +33,7 @@ class StandaloneAuthenticator(object): description = "Standalone Authenticator" - def __init__(self): + def __init__(self, unused_config): self.child_pid = None self.parent_pid = os.getpid() self.subproc_state = None diff --git a/letsencrypt/client/tests/standalone_authenticator_test.py b/letsencrypt/client/tests/standalone_authenticator_test.py index 9adf6a167..62b955e7e 100644 --- a/letsencrypt/client/tests/standalone_authenticator_test.py +++ b/letsencrypt/client/tests/standalone_authenticator_test.py @@ -51,7 +51,7 @@ class ChallPrefTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) def test_chall_pref(self): self.assertEqual(self.authenticator.get_chall_pref("example.com"), @@ -63,7 +63,7 @@ class SNICallbackTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) test_key = pkg_resources.resource_string( __name__, "testdata/rsa256_key.pem") key = le_util.Key("foo", test_key) @@ -106,7 +106,7 @@ class ClientSignalHandlerTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) self.authenticator.tasks = {"foononce.acme.invalid": "stuff"} self.authenticator.child_pid = 12345 @@ -135,7 +135,7 @@ class SubprocSignalHandlerTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) self.authenticator.tasks = {"foononce.acme.invalid": "stuff"} self.authenticator.child_pid = 12345 self.authenticator.parent_pid = 23456 @@ -187,7 +187,7 @@ class AlreadyListeningTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) @mock.patch("letsencrypt.client.standalone_authenticator.psutil." "net_connections") @@ -290,7 +290,7 @@ class PerformTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) test_key = pkg_resources.resource_string( __name__, "testdata/rsa256_key.pem") @@ -367,7 +367,7 @@ class StartListenerTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) @mock.patch("letsencrypt.client.standalone_authenticator." "Crypto.Random.atfork") @@ -402,7 +402,7 @@ class DoParentProcessTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) @mock.patch("letsencrypt.client.standalone_authenticator.signal.signal") @mock.patch("letsencrypt.client.standalone_authenticator." @@ -452,7 +452,7 @@ class DoChildProcessTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) test_key = pkg_resources.resource_string( __name__, "testdata/rsa256_key.pem") key = le_util.Key("foo", test_key) @@ -545,7 +545,7 @@ class CleanupTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import \ StandaloneAuthenticator - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) self.achall = achallenges.DVSNI( chall=challenges.DVSNI(r="whee", nonce="foononce"), domain="foo.example.com", key="key") @@ -575,7 +575,7 @@ class MoreInfoTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import ( StandaloneAuthenticator) - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) def test_more_info(self): """Make sure exceptions aren't raised.""" @@ -587,7 +587,7 @@ class InitTest(unittest.TestCase): def setUp(self): from letsencrypt.client.standalone_authenticator import ( StandaloneAuthenticator) - self.authenticator = StandaloneAuthenticator() + self.authenticator = StandaloneAuthenticator(None) def test_prepare(self): """Make sure exceptions aren't raised. diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 11caf944a..d3c2318d6 100644 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -136,12 +136,10 @@ def main(): # pylint: disable=too-many-branches, too-many-statements if not args.eula: display_eula() - all_auths = [ - configurator.ApacheConfigurator(config), - standalone.StandaloneAuthenticator(), - ] + all_auths = client.init_auths(config) + logging.debug('Initialized authenticators: %s', all_auths) try: - auth = client.determine_authenticator(all_auths) + auth = client.determine_authenticator(all_auths.keys()) except errors.LetsEncryptClientError: logging.critical("No authentication mechanisms were found on your " "system.") diff --git a/setup.py b/setup.py index fac3eef90..c07c1f2ce 100644 --- a/setup.py +++ b/setup.py @@ -119,6 +119,12 @@ setup( 'letsencrypt = letsencrypt.scripts.main:main', 'jws = letsencrypt.acme.jose.jws:CLI.run', ], + 'letsencrypt.authenticators': [ + 'apache = letsencrypt.client.apache.configurator' + ':ApacheConfigurator', + 'standalone = letsencrypt.client.standalone_authenticator' + ':StandaloneAuthenticator', + ], }, zip_safe=False, From 03383c38241bbb4fb0b7b1c438c5652937c8140d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 13:59:33 +0000 Subject: [PATCH 55/92] Fix quotes --- letsencrypt/client/client.py | 4 ++-- letsencrypt/scripts/main.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index a448c10ce..01f5e1c80 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -28,7 +28,7 @@ from letsencrypt.client.display import ops as display_ops from letsencrypt.client.display import enhancements -SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT = 'letsencrypt.authenticators' +SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT = "letsencrypt.authenticators" """Setuptools entry point group name for Authenticator plugins.""" @@ -43,7 +43,7 @@ def init_auths(config): zope.interface.verify.verifyObject(interfaces.IAuthenticator, auth) except zope.interface.exceptions.BrokenImplementation: logging.debug( - '"%s" object does not provide IAuthenticator, skipping', + "%r object does not provide IAuthenticator, skipping", entrypoint.name) else: auths[auth] = entrypoint.name diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index d3c2318d6..d51288c3a 100644 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -137,7 +137,7 @@ def main(): # pylint: disable=too-many-branches, too-many-statements display_eula() all_auths = client.init_auths(config) - logging.debug('Initialized authenticators: %s', all_auths) + logging.debug('Initialized authenticators: %s', all_auths.values()) try: auth = client.determine_authenticator(all_auths.keys()) except errors.LetsEncryptClientError: From d9871b59f032bb64ddd5d65b6dfbb7619d3c7cc5 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 14:42:07 +0000 Subject: [PATCH 56/92] pylint: unused imports --- letsencrypt/scripts/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index d51288c3a..8b2c62935 100644 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -20,8 +20,6 @@ from letsencrypt.client import errors from letsencrypt.client import interfaces from letsencrypt.client import le_util from letsencrypt.client import log -from letsencrypt.client import standalone_authenticator as standalone -from letsencrypt.client.apache import configurator from letsencrypt.client.display import util as display_util from letsencrypt.client.display import ops as display_ops From 3caa0f8453901d9fa541a8cb03e38a82cacf12fb Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 15:03:12 +0000 Subject: [PATCH 57/92] network2: JSON_ERROR_CONTENT_TYPE --- letsencrypt/client/network2.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 6b23e565c..1cd3a0321 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -33,6 +33,7 @@ class Network(object): DER_CONTENT_TYPE = 'application/plix-cert' JSON_CONTENT_TYPE = 'application/json' + JSON_ERROR_CONTENT_TYPE = 'application/problem+json' def __init__(self, new_reg_uri, key, alg=jose.RS256): self.new_reg_uri = new_reg_uri @@ -64,13 +65,13 @@ class Network(object): except ValueError as error: jobj = None - if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: - logging.debug( - 'Decoded JSON response, but wrong Content-Type (%s).', - response_ct) - if not response.ok: if jobj is not None: + if response_ct != cls.JSON_ERROR_CONTENT_TYPE: + logging.debug( + 'Ignoring wrong Content-Type (%r) for JSON Error', + response_ct) + try: raise messages2.Error.from_json(jobj) except jose.DeserializationError as error: @@ -79,10 +80,18 @@ class Network(object): else: # response is not JSON object raise errors.NetworkError(response) - elif (content_type is not None and response_ct != content_type - and content_type != cls.JSON_CONTENT_TYPE): - raise errors.NetworkError( - 'Unexpected response Content-Type: {0}'.format(response_ct)) + else: + if jobj is not None and ( + response_ct != cls.JSON_CONTENT_TYPE or + response_ct != cls.JSON_ERROR_CONTENT_TYPE): + logging.debug( + 'Ignoring wrong Content-Type (%r) for JSON decodable ' + 'response', response_ct) + + if (content_type is not None and response_ct != content_type + and content_type != cls.JSON_CONTENT_TYPE): + raise errors.NetworkError( + 'Unexpected response Content-Type: {0}'.format(response_ct)) def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs): """Send GET request. From df70b327e9783b1d784b431faf8536e3d1e08172 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 15:03:57 +0000 Subject: [PATCH 58/92] StatusValid -> STATUS_VALID --- letsencrypt/client/network2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 1cd3a0321..c1789808d 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -343,7 +343,7 @@ class Network(object): # original Authorization Resource URI only assert updated_authzr.uri == authzr - if updated_authzr.body.status != messages2.StatusValid: + if updated_authzr.body.status != messages2.STATUS_VALID: # push back to the priority queue, with updated retry_after heapq.heappush(waiting, (self._retry_after( response, mintime=mintime), authzr)) From 8e32ae82467a0d2597eb835a3ed8a1d5546e72ad Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 22:00:00 +0000 Subject: [PATCH 59/92] Move init_auths to scripts/main.py. --- letsencrypt/client/client.py | 27 --------------------------- letsencrypt/scripts/main.py | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 01f5e1c80..2f3f9a769 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -1,15 +1,11 @@ """ACME protocol client class and helper functions.""" import logging import os -import pkg_resources import sys import Crypto.PublicKey.RSA import M2Crypto -import zope.interface.exceptions -import zope.interface.verify - from letsencrypt.acme import messages from letsencrypt.acme.jose import util as jose_util @@ -17,7 +13,6 @@ from letsencrypt.client import auth_handler from letsencrypt.client import client_authenticator from letsencrypt.client import crypto_util from letsencrypt.client import errors -from letsencrypt.client import interfaces from letsencrypt.client import le_util from letsencrypt.client import network from letsencrypt.client import reverter @@ -28,28 +23,6 @@ from letsencrypt.client.display import ops as display_ops from letsencrypt.client.display import enhancements -SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT = "letsencrypt.authenticators" -"""Setuptools entry point group name for Authenticator plugins.""" - - -def init_auths(config): - """Find (setuptools entry points) and initialize Authenticators.""" - auths = {} - for entrypoint in pkg_resources.iter_entry_points( - SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT): - auth_cls = entrypoint.load() - auth = auth_cls(config) - try: - zope.interface.verify.verifyObject(interfaces.IAuthenticator, auth) - except zope.interface.exceptions.BrokenImplementation: - logging.debug( - "%r object does not provide IAuthenticator, skipping", - entrypoint.name) - else: - auths[auth] = entrypoint.name - return auths - - class Client(object): """ACME protocol client. diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 8b2c62935..3b4b7c10d 100644 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -11,6 +11,8 @@ import sys import confargparse import zope.component +import zope.interface.exceptions +import zope.interface.verify import letsencrypt @@ -24,6 +26,28 @@ from letsencrypt.client.display import util as display_util from letsencrypt.client.display import ops as display_ops +SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT = "letsencrypt.authenticators" +"""Setuptools entry point group name for Authenticator plugins.""" + + +def init_auths(config): + """Find (setuptools entry points) and initialize Authenticators.""" + auths = {} + for entrypoint in pkg_resources.iter_entry_points( + SETUPTOOLS_AUTHENTICATORS_ENTRY_POINT): + auth_cls = entrypoint.load() + auth = auth_cls(config) + try: + zope.interface.verify.verifyObject(interfaces.IAuthenticator, auth) + except zope.interface.exceptions.BrokenImplementation: + logging.debug( + "%r object does not provide IAuthenticator, skipping", + entrypoint.name) + else: + auths[auth] = entrypoint.name + return auths + + def create_parser(): """Create parser.""" parser = confargparse.ConfArgParser( @@ -134,7 +158,7 @@ def main(): # pylint: disable=too-many-branches, too-many-statements if not args.eula: display_eula() - all_auths = client.init_auths(config) + all_auths = init_auths(config) logging.debug('Initialized authenticators: %s', all_auths.values()) try: auth = client.determine_authenticator(all_auths.keys()) From 6b78789ea3963ba406538d733f9ff65db8337fa7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 26 Mar 2015 22:12:40 +0000 Subject: [PATCH 60/92] Improve plugins.rst --- docs/plugins.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/plugins.rst b/docs/plugins.rst index 552985aab..fafb8d5d3 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -2,4 +2,12 @@ Plugins ======= -You can find an example in ``examples/plugins/`` directory. +Let's Encrypt client supports dynamic discovery of plugins through the +`setuptools entry points`_. This way you can, for example, create a +custom implementation of +`~letsencrypt.client.interfaces.IAuthenticator` without having to +merge it with the core upstream source code. Example is provided in +``examples/plugins/`` directory. + +.. _`setuptools entry points`: + https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins From 2d848994f4aec4a640ac6c526d9ea28f68dc8ff2 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Thu, 26 Mar 2015 17:23:17 -0700 Subject: [PATCH 61/92] Move IAuthenticators and IInstallers into plugins dir --- letsencrypt/client/apache/__init__.py | 1 - letsencrypt/client/client.py | 2 +- letsencrypt/client/constants.py | 2 +- letsencrypt/client/plugins/__init__.py | 1 + letsencrypt/client/plugins/apache/__init__.py | 1 + .../{ => plugins}/apache/configurator.py | 45 ++++--- .../client/{ => plugins}/apache/dvsni.py | 11 +- .../client/{ => plugins}/apache/obj.py | 0 .../{ => plugins}/apache/options-ssl.conf | 0 .../client/{ => plugins}/apache/parser.py | 0 .../apache/tests}/__init__.py | 0 .../apache/tests}/configurator_test.py | 32 +++-- .../apache/tests}/dvsni_test.py | 20 +-- .../apache/tests}/obj_test.py | 14 +- .../apache/tests}/parser_test.py | 20 +-- .../default_vhost/apache2/apache2.conf | 0 .../other-vhosts-access-log.conf | 0 .../apache2/conf-available/security.conf | 0 .../apache2/conf-available/serve-cgi-bin.conf | 0 .../conf-enabled/other-vhosts-access-log.conf | 0 .../apache2/conf-enabled/security.conf | 0 .../apache2/conf-enabled/serve-cgi-bin.conf | 0 .../default_vhost/apache2/envvars | 0 .../apache2/mods-available/ssl.conf | 0 .../apache2/mods-available/ssl.load | 0 .../default_vhost/apache2/ports.conf | 0 .../apache2/sites-available/000-default.conf | 0 .../apache2/sites-available/default-ssl.conf | 0 .../apache2/sites-enabled/000-default.conf | 0 .../debian_apache_2_4/default_vhost/sites | 0 .../two_vhost_80/apache2/apache2.conf | 0 .../other-vhosts-access-log.conf | 0 .../apache2/conf-available/security.conf | 0 .../apache2/conf-available/serve-cgi-bin.conf | 0 .../conf-enabled/other-vhosts-access-log.conf | 0 .../apache2/conf-enabled/security.conf | 0 .../apache2/conf-enabled/serve-cgi-bin.conf | 0 .../two_vhost_80/apache2/envvars | 0 .../apache2/mods-available/ssl.conf | 0 .../apache2/mods-available/ssl.load | 0 .../two_vhost_80/apache2/ports.conf | 0 .../apache2/sites-available/000-default.conf | 0 .../apache2/sites-available/default-ssl.conf | 0 .../sites-available/encryption-example.conf | 0 .../apache2/sites-available/letsencrypt.conf | 0 .../apache2/sites-enabled/000-default.conf | 0 .../sites-enabled/encryption-example.conf | 0 .../apache2/sites-enabled/letsencrypt.conf | 0 .../debian_apache_2_4/two_vhost_80/sites | 0 .../apache => plugins/apache/tests}/util.py | 14 +- .../client/plugins/standalone/__init__.py | 1 + .../standalone/authenticator.py} | 0 .../plugins/standalone/tests/__init__.py | 1 + .../standalone/tests/authenticator_test.py} | 125 ++++++++++-------- letsencrypt/client/tests/client_test.py | 8 +- letsencrypt/client/tests/revoker_test.py | 2 +- setup.py | 11 +- 57 files changed, 173 insertions(+), 138 deletions(-) delete mode 100644 letsencrypt/client/apache/__init__.py create mode 100644 letsencrypt/client/plugins/__init__.py create mode 100644 letsencrypt/client/plugins/apache/__init__.py rename letsencrypt/client/{ => plugins}/apache/configurator.py (96%) rename letsencrypt/client/{ => plugins}/apache/dvsni.py (95%) rename letsencrypt/client/{ => plugins}/apache/obj.py (100%) rename letsencrypt/client/{ => plugins}/apache/options-ssl.conf (100%) rename letsencrypt/client/{ => plugins}/apache/parser.py (100%) rename letsencrypt/client/{tests/apache => plugins/apache/tests}/__init__.py (100%) rename letsencrypt/client/{tests/apache => plugins/apache/tests}/configurator_test.py (87%) rename letsencrypt/client/{tests/apache => plugins/apache/tests}/dvsni_test.py (91%) rename letsencrypt/client/{tests/apache => plugins/apache/tests}/obj_test.py (82%) rename letsencrypt/client/{tests/apache => plugins/apache/tests}/parser_test.py (84%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/apache2.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/other-vhosts-access-log.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/security.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/serve-cgi-bin.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/other-vhosts-access-log.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/security.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/serve-cgi-bin.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/envvars (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.load (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/ports.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/000-default.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/default-ssl.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-enabled/000-default.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/default_vhost/sites (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/apache2.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/other-vhosts-access-log.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/security.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/serve-cgi-bin.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/other-vhosts-access-log.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/security.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/serve-cgi-bin.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/envvars (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.load (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/ports.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/encryption-example.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/000-default.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/encryption-example.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/letsencrypt.conf (100%) rename letsencrypt/client/{ => plugins/apache}/tests/testdata/debian_apache_2_4/two_vhost_80/sites (100%) rename letsencrypt/client/{tests/apache => plugins/apache/tests}/util.py (89%) create mode 100644 letsencrypt/client/plugins/standalone/__init__.py rename letsencrypt/client/{standalone_authenticator.py => plugins/standalone/authenticator.py} (100%) create mode 100644 letsencrypt/client/plugins/standalone/tests/__init__.py rename letsencrypt/client/{tests/standalone_authenticator_test.py => plugins/standalone/tests/authenticator_test.py} (84%) diff --git a/letsencrypt/client/apache/__init__.py b/letsencrypt/client/apache/__init__.py deleted file mode 100644 index f1b2c08e7..000000000 --- a/letsencrypt/client/apache/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Let's Encrypt client.apache.""" diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 01f5e1c80..09817cc21 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -23,7 +23,7 @@ from letsencrypt.client import network from letsencrypt.client import reverter from letsencrypt.client import revoker -from letsencrypt.client.apache import configurator +from letsencrypt.client.plugins.apache import configurator from letsencrypt.client.display import ops as display_ops from letsencrypt.client.display import enhancements diff --git a/letsencrypt/client/constants.py b/letsencrypt/client/constants.py index 3e27d88ac..43cf5e8a0 100644 --- a/letsencrypt/client/constants.py +++ b/letsencrypt/client/constants.py @@ -31,7 +31,7 @@ List of expected options parameters: APACHE_MOD_SSL_CONF = pkg_resources.resource_filename( - "letsencrypt.client.apache", "options-ssl.conf") + "letsencrypt.client.plugins.apache", "options-ssl.conf") """Path to the Apache mod_ssl config file found in the Let's Encrypt distribution.""" diff --git a/letsencrypt/client/plugins/__init__.py b/letsencrypt/client/plugins/__init__.py new file mode 100644 index 000000000..538189015 --- /dev/null +++ b/letsencrypt/client/plugins/__init__.py @@ -0,0 +1 @@ +"""Let's Encrypt client.plugins.""" diff --git a/letsencrypt/client/plugins/apache/__init__.py b/letsencrypt/client/plugins/apache/__init__.py new file mode 100644 index 000000000..70172b06d --- /dev/null +++ b/letsencrypt/client/plugins/apache/__init__.py @@ -0,0 +1 @@ +"""Let's Encrypt client.plugins.apache.""" diff --git a/letsencrypt/client/apache/configurator.py b/letsencrypt/client/plugins/apache/configurator.py similarity index 96% rename from letsencrypt/client/apache/configurator.py rename to letsencrypt/client/plugins/apache/configurator.py index 89a2ff4e2..5b682216b 100644 --- a/letsencrypt/client/apache/configurator.py +++ b/letsencrypt/client/plugins/apache/configurator.py @@ -18,9 +18,9 @@ from letsencrypt.client import errors from letsencrypt.client import interfaces from letsencrypt.client import le_util -from letsencrypt.client.apache import dvsni -from letsencrypt.client.apache import obj -from letsencrypt.client.apache import parser +from letsencrypt.client.plugins.apache import dvsni +from letsencrypt.client.plugins.apache import obj +from letsencrypt.client.plugins.apache import parser # TODO: Augeas sections ie. , beginning and closing @@ -68,11 +68,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :type config: :class:`~letsencrypt.client.interfaces.IConfig` :ivar parser: Handles low level parsing - :type parser: :class:`letsencrypt.client.apache.parser` + :type parser: :class:`letsencrypt.client.plugins.apache.parser` :ivar tup version: version of Apache :ivar list vhosts: All vhosts found in the configuration - (:class:`list` of :class:`letsencrypt.client.apache.obj.VirtualHost`) + (:class:`list` of + :class:`letsencrypt.client.plugins.apache.obj.VirtualHost`) :ivar dict assoc: Mapping between domains and vhosts @@ -203,7 +204,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str target_name: domain name :returns: ssl vhost associated with name - :rtype: :class:`letsencrypt.client.apache.obj.VirtualHost` + :rtype: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` """ # Allows for domain names to be associated with a virtual host @@ -244,7 +245,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str domain: domain name to associate :param vhost: virtual host to associate with domain - :type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost` + :type vhost: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` """ self.assoc[domain] = vhost @@ -281,7 +282,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Helper function for get_virtual_hosts(). :param host: In progress vhost whose names will be added - :type host: :class:`letsencrypt.client.apache.obj.VirtualHost` + :type host: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` """ name_match = self.aug.match(("%s//*[self::directive=~regexp('%s')] | " @@ -302,7 +303,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str path: Augeas path to virtual host :returns: newly created vhost - :rtype: :class:`letsencrypt.client.apache.obj.VirtualHost` + :rtype: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` """ addrs = set() @@ -326,7 +327,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Returns list of virtual hosts found in the Apache configuration. :returns: List of - :class:`letsencrypt.client.apache.obj.VirtualHost` objects + :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` objects found in configuration :rtype: list @@ -404,7 +405,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Checks to see if the server is ready for SNI challenges. :param vhost: VirtualHost to check SNI compatibility - :type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost` + :type vhost: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` :param str default_addr: TODO - investigate function further @@ -436,10 +437,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): .. note:: This function saves the configuration :param nonssl_vhost: Valid VH that doesn't have SSLEngine on - :type nonssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost` + :type nonssl_vhost: :class:`~apache.obj.VirtualHost` :returns: SSL vhost - :rtype: :class:`letsencrypt.client.apache.obj.VirtualHost` + :rtype: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` """ avail_fp = nonssl_vhost.filep @@ -559,13 +560,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): .. note:: This function saves the configuration :param ssl_vhost: Destination of traffic, an ssl enabled vhost - :type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost` + :type ssl_vhost: :class:`~apache.obj.VirtualHost` :param unused_options: Not currently used :type unused_options: Not Available :returns: Success, general_vhost (HTTP vhost) - :rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`) + :rtype: (bool, :class:`~apache.obj.VirtualHost`) """ if not mod_loaded("rewrite_module", self.config.apache_ctl): @@ -617,7 +618,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): -1 is also returned in case of no redirection/rewrite directives :param vhost: vhost to check - :type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost` + :type vhost: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` :returns: Success, code value... see documentation :rtype: bool, int @@ -649,10 +650,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Creates an http_vhost specifically to redirect for the ssl_vhost. :param ssl_vhost: ssl vhost - :type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost` + :type ssl_vhost: :class:`~apache.obj.VirtualHost` :returns: Success, vhost - :rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`) + :rtype: (bool, :class:`~apache.obj.VirtualHost`) """ # Consider changing this to a dictionary check @@ -734,7 +735,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not conflict: returns space separated list of new host addrs :param ssl_vhost: SSL Vhost to check for possible port 80 redirection - :type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost` + :type ssl_vhost: :class:`~apache.obj.VirtualHost` :returns: TODO :rtype: TODO @@ -767,10 +768,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): Consider changing this into a dict check :param ssl_vhost: ssl vhost to check - :type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost` + :type ssl_vhost: :class:`~apache.obj.VirtualHost` :returns: HTTP vhost or None if unsuccessful - :rtype: :class:`letsencrypt.client.apache.obj.VirtualHost` or None + :rtype: :class:`~apache.obj.VirtualHost` or None """ # _default_:443 check @@ -860,7 +861,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): .. todo:: Make sure link is not broken... :param vhost: vhost to enable - :type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost` + :type vhost: :class:`~apache.obj.VirtualHost` :returns: Success :rtype: bool diff --git a/letsencrypt/client/apache/dvsni.py b/letsencrypt/client/plugins/apache/dvsni.py similarity index 95% rename from letsencrypt/client/apache/dvsni.py rename to letsencrypt/client/plugins/apache/dvsni.py index 71bd03c7e..2e1c948aa 100644 --- a/letsencrypt/client/apache/dvsni.py +++ b/letsencrypt/client/plugins/apache/dvsni.py @@ -2,20 +2,19 @@ import logging import os -from letsencrypt.client.apache import parser +from letsencrypt.client.plugins.apache import parser class ApacheDvsni(object): """Class performs DVSNI challenges within the Apache configurator. :ivar configurator: ApacheConfigurator object - :type configurator: - :class:`letsencrypt.client.apache.configurator.ApacheConfigurator` + :type configurator: :class:`~apache.configurator.ApacheConfigurator` :ivar list achalls: Annotated :class:`~letsencrypt.client.achallenges.DVSNI` challenges. - :param list indicies: Meant to hold indices of challenges in a + :param list indices: Meant to hold indices of challenges in a larger array. ApacheDvsni is capable of solving many challenges at once which causes an indexing issue within ApacheConfigurator who must return all responses in order. Imagine ApacheConfigurator @@ -129,7 +128,7 @@ class ApacheDvsni(object): Result: Apache config includes virtual servers for issued challs :param list ll_addrs: list of list of - :class:`letsencrypt.client.apache.obj.Addr` to apply + :class:`letsencrypt.client.plugins.apache.obj.Addr` to apply """ # TODO: Use ip address of existing vhost instead of relying on FQDN @@ -168,7 +167,7 @@ class ApacheDvsni(object): :type achall: :class:`letsencrypt.client.achallenges.DVSNI` :param list ip_addrs: addresses of challenged domain - :class:`list` of type :class:`letsencrypt.client.apache.obj.Addr` + :class:`list` of type :class:`~apache.obj.Addr` :returns: virtual host configuration text :rtype: str diff --git a/letsencrypt/client/apache/obj.py b/letsencrypt/client/plugins/apache/obj.py similarity index 100% rename from letsencrypt/client/apache/obj.py rename to letsencrypt/client/plugins/apache/obj.py diff --git a/letsencrypt/client/apache/options-ssl.conf b/letsencrypt/client/plugins/apache/options-ssl.conf similarity index 100% rename from letsencrypt/client/apache/options-ssl.conf rename to letsencrypt/client/plugins/apache/options-ssl.conf diff --git a/letsencrypt/client/apache/parser.py b/letsencrypt/client/plugins/apache/parser.py similarity index 100% rename from letsencrypt/client/apache/parser.py rename to letsencrypt/client/plugins/apache/parser.py diff --git a/letsencrypt/client/tests/apache/__init__.py b/letsencrypt/client/plugins/apache/tests/__init__.py similarity index 100% rename from letsencrypt/client/tests/apache/__init__.py rename to letsencrypt/client/plugins/apache/tests/__init__.py diff --git a/letsencrypt/client/tests/apache/configurator_test.py b/letsencrypt/client/plugins/apache/tests/configurator_test.py similarity index 87% rename from letsencrypt/client/tests/apache/configurator_test.py rename to letsencrypt/client/plugins/apache/tests/configurator_test.py index 1bb4207a3..0b7d4f570 100644 --- a/letsencrypt/client/tests/apache/configurator_test.py +++ b/letsencrypt/client/plugins/apache/tests/configurator_test.py @@ -1,4 +1,4 @@ -"""Test for letsencrypt.client.apache.configurator.""" +"""Test for letsencrypt.client.plugins.apache.configurator.""" import os import re import shutil @@ -12,11 +12,11 @@ from letsencrypt.client import achallenges from letsencrypt.client import errors from letsencrypt.client import le_util -from letsencrypt.client.apache import configurator -from letsencrypt.client.apache import obj -from letsencrypt.client.apache import parser +from letsencrypt.client.plugins.apache import configurator +from letsencrypt.client.plugins.apache import obj +from letsencrypt.client.plugins.apache import parser -from letsencrypt.client.tests.apache import util +from letsencrypt.client.plugins.apache.tests import util class TwoVhost80Test(util.ApacheTest): @@ -25,7 +25,7 @@ class TwoVhost80Test(util.ApacheTest): def setUp(self): super(TwoVhost80Test, self).setUp() - with mock.patch("letsencrypt.client.apache.configurator." + with mock.patch("letsencrypt.client.plugins.apache.configurator." "mod_loaded") as mock_load: mock_load.return_value = True self.config = util.get_apache_configurator( @@ -46,6 +46,12 @@ class TwoVhost80Test(util.ApacheTest): ['letsencrypt.demo', 'encryption-example.demo', 'ip-172-30-0-17'])) def test_get_virtual_hosts(self): + """Make sure all vhosts are being properly found. + + .. note:: If test fails, only finding 1 Vhost... it is likely that + it is a problem with is_enabled. + + """ vhs = self.config.get_virtual_hosts() self.assertEqual(len(vhs), 4) found = 0 @@ -59,6 +65,14 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual(found, 4) def test_is_site_enabled(self): + """Test if site is enabled. + + .. note:: This test currently fails for hard links + (which may happen if you move dirs incorrectly) + .. warning:: This test does not work when running using the + unittest.main() function. It incorrectly copies symlinks. + + """ self.assertTrue(self.config.is_site_enabled(self.vh_truth[0].filep)) self.assertFalse(self.config.is_site_enabled(self.vh_truth[1].filep)) self.assertTrue(self.config.is_site_enabled(self.vh_truth[2].filep)) @@ -134,9 +148,9 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual(len(self.config.vhosts), 5) - @mock.patch("letsencrypt.client.apache.configurator." + @mock.patch("letsencrypt.client.plugins.apache.configurator." "dvsni.ApacheDvsni.perform") - @mock.patch("letsencrypt.client.apache.configurator." + @mock.patch("letsencrypt.client.plugins.apache.configurator." "ApacheConfigurator.restart") def test_perform(self, mock_restart, mock_dvsni_perform): # Only tests functionality specific to configurator.perform @@ -166,7 +180,7 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual(mock_restart.call_count, 1) - @mock.patch("letsencrypt.client.apache.configurator." + @mock.patch("letsencrypt.client.plugins.apache.configurator." "subprocess.Popen") def test_get_version(self, mock_popen): mock_popen().communicate.return_value = ( diff --git a/letsencrypt/client/tests/apache/dvsni_test.py b/letsencrypt/client/plugins/apache/tests/dvsni_test.py similarity index 91% rename from letsencrypt/client/tests/apache/dvsni_test.py rename to letsencrypt/client/plugins/apache/tests/dvsni_test.py index f3e0e9ce5..9bddfc481 100644 --- a/letsencrypt/client/tests/apache/dvsni_test.py +++ b/letsencrypt/client/plugins/apache/tests/dvsni_test.py @@ -1,4 +1,4 @@ -"""Test for letsencrypt.client.apache.dvsni.""" +"""Test for letsencrypt.client.plugins.apache.dvsni.""" import pkg_resources import unittest import shutil @@ -10,9 +10,9 @@ from letsencrypt.acme import challenges from letsencrypt.client import achallenges from letsencrypt.client import le_util -from letsencrypt.client.apache.obj import Addr +from letsencrypt.client.plugins.apache.obj import Addr -from letsencrypt.client.tests.apache import util +from letsencrypt.client.plugins.apache.tests import util class DvsniPerformTest(util.ApacheTest): @@ -21,20 +21,20 @@ class DvsniPerformTest(util.ApacheTest): def setUp(self): super(DvsniPerformTest, self).setUp() - with mock.patch("letsencrypt.client.apache.configurator." + with mock.patch("letsencrypt.client.plugins.apache.configurator." "mod_loaded") as mock_load: mock_load.return_value = True config = util.get_apache_configurator( self.config_path, self.config_dir, self.work_dir, self.ssl_options) - from letsencrypt.client.apache import dvsni + from letsencrypt.client.plugins.apache import dvsni self.sni = dvsni.ApacheDvsni(config) rsa256_file = pkg_resources.resource_filename( - "letsencrypt.client.tests", 'testdata/rsa256_key.pem') + "letsencrypt.client.tests", "testdata/rsa256_key.pem") rsa256_pem = pkg_resources.resource_string( - "letsencrypt.client.tests", 'testdata/rsa256_key.pem') + "letsencrypt.client.tests", "testdata/rsa256_key.pem") auth_key = le_util.Key(rsa256_file, rsa256_pem) self.achalls = [ @@ -74,7 +74,7 @@ class DvsniPerformTest(util.ApacheTest): nonce_domain=self.achalls[0].nonce_domain) achall.gen_cert_and_response.return_value = ("pem", response) - with mock.patch("letsencrypt.client.apache.dvsni.open", + with mock.patch("letsencrypt.client.plugins.apache.dvsni.open", m_open, create=True): # pylint: disable=protected-access self.assertEqual(response, self.sni._setup_challenge_cert( @@ -82,7 +82,7 @@ class DvsniPerformTest(util.ApacheTest): self.assertTrue(m_open.called) self.assertEqual( - m_open.call_args[0], (self.sni.get_cert_file(achall), 'w')) + m_open.call_args[0], (self.sni.get_cert_file(achall), "w")) self.assertEqual(m_open().write.call_args[0][0], "pem") def test_perform1(self): @@ -166,5 +166,5 @@ class DvsniPerformTest(util.ApacheTest): set([self.achalls[1].nonce_domain])) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/letsencrypt/client/tests/apache/obj_test.py b/letsencrypt/client/plugins/apache/tests/obj_test.py similarity index 82% rename from letsencrypt/client/tests/apache/obj_test.py rename to letsencrypt/client/plugins/apache/tests/obj_test.py index 070fa7b11..b0c65eadb 100644 --- a/letsencrypt/client/tests/apache/obj_test.py +++ b/letsencrypt/client/plugins/apache/tests/obj_test.py @@ -1,11 +1,11 @@ -"""Test the helper objects in apache.obj.py.""" +"""Test the helper objects in letsencrypt.client.plugins.apache.obj.""" import unittest class AddrTest(unittest.TestCase): """Test the Addr class.""" def setUp(self): - from letsencrypt.client.apache.obj import Addr + from letsencrypt.client.plugins.apache.obj import Addr self.addr1 = Addr.fromstring("192.168.1.1") self.addr2 = Addr.fromstring("192.168.1.1:*") self.addr3 = Addr.fromstring("192.168.1.1:80") @@ -34,7 +34,7 @@ class AddrTest(unittest.TestCase): self.assertFalse(self.addr1 == 3333) def test_set_inclusion(self): - from letsencrypt.client.apache.obj import Addr + from letsencrypt.client.plugins.apache.obj import Addr set_a = set([self.addr1, self.addr2]) addr1b = Addr.fromstring("192.168.1.1") addr2b = Addr.fromstring("192.168.1.1:*") @@ -46,15 +46,15 @@ class AddrTest(unittest.TestCase): class VirtualHostTest(unittest.TestCase): """Test the VirtualHost class.""" def setUp(self): - from letsencrypt.client.apache.obj import VirtualHost - from letsencrypt.client.apache.obj import Addr + from letsencrypt.client.plugins.apache.obj import VirtualHost + from letsencrypt.client.plugins.apache.obj import Addr self.vhost1 = VirtualHost( "filep", "vh_path", set([Addr.fromstring("localhost")]), False, False) def test_eq(self): - from letsencrypt.client.apache.obj import Addr - from letsencrypt.client.apache.obj import VirtualHost + from letsencrypt.client.plugins.apache.obj import Addr + from letsencrypt.client.plugins.apache.obj import VirtualHost vhost1b = VirtualHost( "filep", "vh_path", set([Addr.fromstring("localhost")]), False, False) diff --git a/letsencrypt/client/tests/apache/parser_test.py b/letsencrypt/client/plugins/apache/tests/parser_test.py similarity index 84% rename from letsencrypt/client/tests/apache/parser_test.py rename to letsencrypt/client/plugins/apache/tests/parser_test.py index f30927886..d394feeaa 100644 --- a/letsencrypt/client/tests/apache/parser_test.py +++ b/letsencrypt/client/plugins/apache/tests/parser_test.py @@ -1,4 +1,4 @@ -"""Tests the ApacheParser class.""" +"""Tests for letsencrypt.client.plugins.apache.parser.""" import os import shutil import sys @@ -11,7 +11,7 @@ import zope.component from letsencrypt.client import errors from letsencrypt.client.display import util as display_util -from letsencrypt.client.tests.apache import util +from letsencrypt.client.plugins.apache.tests import util class ApacheParserTest(util.ApacheTest): @@ -22,7 +22,7 @@ class ApacheParserTest(util.ApacheTest): zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) - from letsencrypt.client.apache.parser import ApacheParser + from letsencrypt.client.plugins.apache.parser import ApacheParser self.aug = augeas.Augeas(flags=augeas.Augeas.NONE) self.parser = ApacheParser(self.aug, self.config_path, self.ssl_options) @@ -32,19 +32,19 @@ class ApacheParserTest(util.ApacheTest): shutil.rmtree(self.work_dir) def test_root_normalized(self): - from letsencrypt.client.apache.parser import ApacheParser + from letsencrypt.client.plugins.apache.parser import ApacheParser path = os.path.join(self.temp_dir, "debian_apache_2_4/////" "two_vhost_80/../two_vhost_80/apache2") parser = ApacheParser(self.aug, path, None) self.assertEqual(parser.root, self.config_path) def test_root_absolute(self): - from letsencrypt.client.apache.parser import ApacheParser + from letsencrypt.client.plugins.apache.parser import ApacheParser parser = ApacheParser(self.aug, os.path.relpath(self.config_path), None) self.assertEqual(parser.root, self.config_path) def test_root_no_trailing_slash(self): - from letsencrypt.client.apache.parser import ApacheParser + from letsencrypt.client.plugins.apache.parser import ApacheParser parser = ApacheParser(self.aug, self.config_path + os.path.sep, None) self.assertEqual(parser.root, self.config_path) @@ -67,7 +67,7 @@ class ApacheParserTest(util.ApacheTest): self.assertTrue(matches) def test_find_dir(self): - from letsencrypt.client.apache.parser import case_i + from letsencrypt.client.plugins.apache.parser import case_i test = self.parser.find_dir(case_i("Listen"), "443") # This will only look in enabled hosts test2 = self.parser.find_dir(case_i("documentroot")) @@ -92,7 +92,7 @@ class ApacheParserTest(util.ApacheTest): Path must be valid before attempting to add to augeas """ - from letsencrypt.client.apache.parser import get_aug_path + from letsencrypt.client.plugins.apache.parser import get_aug_path self.parser.add_dir_to_ifmodssl( get_aug_path(self.parser.loc["default"]), "FakeDirective", "123") @@ -103,11 +103,11 @@ class ApacheParserTest(util.ApacheTest): self.assertTrue("IfModule" in matches[0]) def test_get_aug_path(self): - from letsencrypt.client.apache.parser import get_aug_path + from letsencrypt.client.plugins.apache.parser import get_aug_path self.assertEqual("/files/etc/apache", get_aug_path("/etc/apache")) def test_set_locations(self): - with mock.patch("letsencrypt.client.apache.parser." + with mock.patch("letsencrypt.client.plugins.apache.parser." "os.path") as mock_path: mock_path.isfile.return_value = False diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/apache2.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/apache2.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/apache2.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/apache2.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/other-vhosts-access-log.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/other-vhosts-access-log.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/other-vhosts-access-log.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/other-vhosts-access-log.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/security.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/security.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/security.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/security.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/serve-cgi-bin.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/serve-cgi-bin.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/serve-cgi-bin.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/serve-cgi-bin.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/other-vhosts-access-log.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/other-vhosts-access-log.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/other-vhosts-access-log.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/other-vhosts-access-log.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/security.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/security.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/security.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/security.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/serve-cgi-bin.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/serve-cgi-bin.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/serve-cgi-bin.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/serve-cgi-bin.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/envvars b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/envvars similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/envvars rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/envvars diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.load b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.load similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.load rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.load diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/ports.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/ports.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/ports.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/ports.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/000-default.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/000-default.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/000-default.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/000-default.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/default-ssl.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/default-ssl.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/default-ssl.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/default-ssl.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-enabled/000-default.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-enabled/000-default.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-enabled/000-default.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-enabled/000-default.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/sites b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/sites similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/default_vhost/sites rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/default_vhost/sites diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/apache2.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/apache2.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/apache2.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/apache2.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/other-vhosts-access-log.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/other-vhosts-access-log.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/other-vhosts-access-log.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/other-vhosts-access-log.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/security.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/security.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/security.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/security.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/serve-cgi-bin.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/serve-cgi-bin.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/serve-cgi-bin.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/serve-cgi-bin.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/other-vhosts-access-log.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/other-vhosts-access-log.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/other-vhosts-access-log.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/other-vhosts-access-log.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/security.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/security.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/security.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/security.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/serve-cgi-bin.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/serve-cgi-bin.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/serve-cgi-bin.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/serve-cgi-bin.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/envvars b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/envvars similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/envvars rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/envvars diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.load b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.load similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.load rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.load diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/ports.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/ports.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/ports.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/ports.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/encryption-example.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/encryption-example.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/encryption-example.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/encryption-example.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/000-default.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/000-default.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/000-default.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/000-default.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/encryption-example.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/encryption-example.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/encryption-example.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/encryption-example.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/letsencrypt.conf b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/letsencrypt.conf similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/letsencrypt.conf rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/letsencrypt.conf diff --git a/letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/sites b/letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/sites similarity index 100% rename from letsencrypt/client/tests/testdata/debian_apache_2_4/two_vhost_80/sites rename to letsencrypt/client/plugins/apache/tests/testdata/debian_apache_2_4/two_vhost_80/sites diff --git a/letsencrypt/client/tests/apache/util.py b/letsencrypt/client/plugins/apache/tests/util.py similarity index 89% rename from letsencrypt/client/tests/apache/util.py rename to letsencrypt/client/plugins/apache/tests/util.py index 6e8cf7d53..d1ba17f5a 100644 --- a/letsencrypt/client/tests/apache/util.py +++ b/letsencrypt/client/plugins/apache/tests/util.py @@ -1,4 +1,4 @@ -"""Common utilities for letsencrypt.client.apache.""" +"""Common utilities for letsencrypt.client.plugins.apache.""" import os import pkg_resources import shutil @@ -8,8 +8,8 @@ import unittest import mock from letsencrypt.client import constants -from letsencrypt.client.apache import configurator -from letsencrypt.client.apache import obj +from letsencrypt.client.plugins.apache import configurator +from letsencrypt.client.plugins.apache import obj class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods @@ -26,9 +26,9 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2") self.rsa256_file = pkg_resources.resource_filename( - "letsencrypt.client.tests", 'testdata/rsa256_key.pem') + "letsencrypt.client.tests", "testdata/rsa256_key.pem") self.rsa256_pem = pkg_resources.resource_string( - "letsencrypt.client.tests", 'testdata/rsa256_key.pem') + "letsencrypt.client.tests", "testdata/rsa256_key.pem") def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"): @@ -38,7 +38,7 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"): work_dir = tempfile.mkdtemp("work") test_configs = pkg_resources.resource_filename( - "letsencrypt.client.tests", "testdata/%s" % test_dir) + "letsencrypt.client.plugins.apache.tests", "testdata/%s" % test_dir) shutil.copytree( test_configs, os.path.join(temp_dir, test_dir), symlinks=True) @@ -59,7 +59,7 @@ def get_apache_configurator( backups = os.path.join(work_dir, "backups") - with mock.patch("letsencrypt.client.apache.configurator." + with mock.patch("letsencrypt.client.plugins.apache.configurator." "subprocess.Popen") as mock_popen: # This just states that the ssl module is already loaded mock_popen().communicate.return_value = ("ssl_module", "") diff --git a/letsencrypt/client/plugins/standalone/__init__.py b/letsencrypt/client/plugins/standalone/__init__.py new file mode 100644 index 000000000..41de6eaf7 --- /dev/null +++ b/letsencrypt/client/plugins/standalone/__init__.py @@ -0,0 +1 @@ +"""Let's Encrypt client.plugins.standalone.""" diff --git a/letsencrypt/client/standalone_authenticator.py b/letsencrypt/client/plugins/standalone/authenticator.py similarity index 100% rename from letsencrypt/client/standalone_authenticator.py rename to letsencrypt/client/plugins/standalone/authenticator.py diff --git a/letsencrypt/client/plugins/standalone/tests/__init__.py b/letsencrypt/client/plugins/standalone/tests/__init__.py new file mode 100644 index 000000000..059cd2780 --- /dev/null +++ b/letsencrypt/client/plugins/standalone/tests/__init__.py @@ -0,0 +1 @@ +"""Let's Encrypt Standalone Tests""" diff --git a/letsencrypt/client/tests/standalone_authenticator_test.py b/letsencrypt/client/plugins/standalone/tests/authenticator_test.py similarity index 84% rename from letsencrypt/client/tests/standalone_authenticator_test.py rename to letsencrypt/client/plugins/standalone/tests/authenticator_test.py index 62b955e7e..390d21b9f 100644 --- a/letsencrypt/client/tests/standalone_authenticator_test.py +++ b/letsencrypt/client/plugins/standalone/tests/authenticator_test.py @@ -1,4 +1,4 @@ -"""Tests for letsencrypt.client.standalone_authenticator.""" +"""Tests for letsencrypt.client.plugins.standalone.authenticator.""" import os import pkg_resources import psutil @@ -49,7 +49,7 @@ class CallableExhausted(Exception): class ChallPrefTest(unittest.TestCase): """Tests for chall_pref() method.""" def setUp(self): - from letsencrypt.client.standalone_authenticator import \ + from letsencrypt.client.plugins.standalone.authenticator import \ StandaloneAuthenticator self.authenticator = StandaloneAuthenticator(None) @@ -61,11 +61,11 @@ class ChallPrefTest(unittest.TestCase): class SNICallbackTest(unittest.TestCase): """Tests for sni_callback() method.""" def setUp(self): - from letsencrypt.client.standalone_authenticator import \ + from letsencrypt.client.plugins.standalone.authenticator import \ StandaloneAuthenticator self.authenticator = StandaloneAuthenticator(None) test_key = pkg_resources.resource_string( - __name__, "testdata/rsa256_key.pem") + "letsencrypt.client.tests", "testdata/rsa256_key.pem") key = le_util.Key("foo", test_key) self.cert = achallenges.DVSNI( chall=challenges.DVSNI(r="x"*32, nonce="abcdef"), @@ -104,7 +104,7 @@ class SNICallbackTest(unittest.TestCase): class ClientSignalHandlerTest(unittest.TestCase): """Tests for client_signal_handler() method.""" def setUp(self): - from letsencrypt.client.standalone_authenticator import \ + from letsencrypt.client.plugins.standalone.authenticator import \ StandaloneAuthenticator self.authenticator = StandaloneAuthenticator(None) self.authenticator.tasks = {"foononce.acme.invalid": "stuff"} @@ -133,15 +133,15 @@ class ClientSignalHandlerTest(unittest.TestCase): class SubprocSignalHandlerTest(unittest.TestCase): """Tests for subproc_signal_handler() method.""" def setUp(self): - from letsencrypt.client.standalone_authenticator import \ + from letsencrypt.client.plugins.standalone.authenticator import \ StandaloneAuthenticator self.authenticator = StandaloneAuthenticator(None) self.authenticator.tasks = {"foononce.acme.invalid": "stuff"} self.authenticator.child_pid = 12345 self.authenticator.parent_pid = 23456 - @mock.patch("letsencrypt.client.standalone_authenticator.os.kill") - @mock.patch("letsencrypt.client.standalone_authenticator.sys.exit") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit") def test_subproc_signal_handler(self, mock_exit, mock_kill): self.authenticator.ssl_conn = mock.MagicMock() self.authenticator.connection = mock.MagicMock() @@ -155,8 +155,8 @@ class SubprocSignalHandlerTest(unittest.TestCase): self.authenticator.parent_pid, signal.SIGUSR1) mock_exit.assert_called_once_with(0) - @mock.patch("letsencrypt.client.standalone_authenticator.os.kill") - @mock.patch("letsencrypt.client.standalone_authenticator.sys.exit") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit") def test_subproc_signal_handler_trouble(self, mock_exit, mock_kill): """Test attempting to shut down a non-existent connection. @@ -185,14 +185,15 @@ class SubprocSignalHandlerTest(unittest.TestCase): class AlreadyListeningTest(unittest.TestCase): """Tests for already_listening() method.""" def setUp(self): - from letsencrypt.client.standalone_authenticator import \ + from letsencrypt.client.plugins.standalone.authenticator import \ StandaloneAuthenticator self.authenticator = StandaloneAuthenticator(None) - @mock.patch("letsencrypt.client.standalone_authenticator.psutil." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil." "net_connections") - @mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process") - @mock.patch("letsencrypt.client.standalone_authenticator." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "psutil.Process") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." "zope.component.getUtility") def test_race_condition(self, mock_get_utility, mock_process, mock_net): # This tests a race condition, or permission problem, or OS @@ -216,10 +217,11 @@ class AlreadyListeningTest(unittest.TestCase): self.assertEqual(mock_get_utility.generic_notification.call_count, 0) mock_process.assert_called_once_with(4416) - @mock.patch("letsencrypt.client.standalone_authenticator.psutil." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil." "net_connections") - @mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process") - @mock.patch("letsencrypt.client.standalone_authenticator." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "psutil.Process") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." "zope.component.getUtility") def test_not_listening(self, mock_get_utility, mock_process, mock_net): from psutil._common import sconn @@ -236,10 +238,11 @@ class AlreadyListeningTest(unittest.TestCase): self.assertEqual(mock_get_utility.generic_notification.call_count, 0) self.assertEqual(mock_process.call_count, 0) - @mock.patch("letsencrypt.client.standalone_authenticator.psutil." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil." "net_connections") - @mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process") - @mock.patch("letsencrypt.client.standalone_authenticator." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "psutil.Process") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." "zope.component.getUtility") def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net): from psutil._common import sconn @@ -259,10 +262,11 @@ class AlreadyListeningTest(unittest.TestCase): self.assertEqual(mock_get_utility.call_count, 1) mock_process.assert_called_once_with(4416) - @mock.patch("letsencrypt.client.standalone_authenticator.psutil." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil." "net_connections") - @mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process") - @mock.patch("letsencrypt.client.standalone_authenticator." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "psutil.Process") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." "zope.component.getUtility") def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net): from psutil._common import sconn @@ -288,12 +292,12 @@ class AlreadyListeningTest(unittest.TestCase): class PerformTest(unittest.TestCase): """Tests for perform() method.""" def setUp(self): - from letsencrypt.client.standalone_authenticator import \ + from letsencrypt.client.plugins.standalone.authenticator import \ StandaloneAuthenticator self.authenticator = StandaloneAuthenticator(None) test_key = pkg_resources.resource_string( - __name__, "testdata/rsa256_key.pem") + "letsencrypt.client.tests", "testdata/rsa256_key.pem") self.key = le_util.Key("something", test_key) self.achall1 = achallenges.DVSNI( @@ -365,13 +369,13 @@ class PerformTest(unittest.TestCase): class StartListenerTest(unittest.TestCase): """Tests for start_listener() method.""" def setUp(self): - from letsencrypt.client.standalone_authenticator import \ + from letsencrypt.client.plugins.standalone.authenticator import \ StandaloneAuthenticator self.authenticator = StandaloneAuthenticator(None) - @mock.patch("letsencrypt.client.standalone_authenticator." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." "Crypto.Random.atfork") - @mock.patch("letsencrypt.client.standalone_authenticator.os.fork") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.fork") def test_start_listener_fork_parent(self, mock_fork, mock_atfork): self.authenticator.do_parent_process = mock.Mock() self.authenticator.do_parent_process.return_value = True @@ -384,9 +388,9 @@ class StartListenerTest(unittest.TestCase): self.authenticator.do_parent_process.assert_called_once_with(1717) mock_atfork.assert_called_once_with() - @mock.patch("letsencrypt.client.standalone_authenticator." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." "Crypto.Random.atfork") - @mock.patch("letsencrypt.client.standalone_authenticator.os.fork") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.fork") def test_start_listener_fork_child(self, mock_fork, mock_atfork): self.authenticator.do_parent_process = mock.Mock() self.authenticator.do_child_process = mock.Mock() @@ -400,12 +404,13 @@ class StartListenerTest(unittest.TestCase): class DoParentProcessTest(unittest.TestCase): """Tests for do_parent_process() method.""" def setUp(self): - from letsencrypt.client.standalone_authenticator import \ + from letsencrypt.client.plugins.standalone.authenticator import \ StandaloneAuthenticator self.authenticator = StandaloneAuthenticator(None) - @mock.patch("letsencrypt.client.standalone_authenticator.signal.signal") - @mock.patch("letsencrypt.client.standalone_authenticator." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "signal.signal") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." "zope.component.getUtility") def test_do_parent_process_ok(self, mock_get_utility, mock_signal): self.authenticator.subproc_state = "ready" @@ -414,8 +419,9 @@ class DoParentProcessTest(unittest.TestCase): self.assertEqual(mock_get_utility.call_count, 1) self.assertEqual(mock_signal.call_count, 3) - @mock.patch("letsencrypt.client.standalone_authenticator.signal.signal") - @mock.patch("letsencrypt.client.standalone_authenticator." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "signal.signal") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." "zope.component.getUtility") def test_do_parent_process_inuse(self, mock_get_utility, mock_signal): self.authenticator.subproc_state = "inuse" @@ -424,8 +430,9 @@ class DoParentProcessTest(unittest.TestCase): self.assertEqual(mock_get_utility.call_count, 1) self.assertEqual(mock_signal.call_count, 3) - @mock.patch("letsencrypt.client.standalone_authenticator.signal.signal") - @mock.patch("letsencrypt.client.standalone_authenticator." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "signal.signal") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." "zope.component.getUtility") def test_do_parent_process_cantbind(self, mock_get_utility, mock_signal): self.authenticator.subproc_state = "cantbind" @@ -434,8 +441,9 @@ class DoParentProcessTest(unittest.TestCase): self.assertEqual(mock_get_utility.call_count, 1) self.assertEqual(mock_signal.call_count, 3) - @mock.patch("letsencrypt.client.standalone_authenticator.signal.signal") - @mock.patch("letsencrypt.client.standalone_authenticator." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "signal.signal") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." "zope.component.getUtility") def test_do_parent_process_timeout(self, mock_get_utility, mock_signal): # Normally times out in 5 seconds and returns False. We can @@ -450,11 +458,11 @@ class DoParentProcessTest(unittest.TestCase): class DoChildProcessTest(unittest.TestCase): """Tests for do_child_process() method.""" def setUp(self): - from letsencrypt.client.standalone_authenticator import \ + from letsencrypt.client.plugins.standalone.authenticator import \ StandaloneAuthenticator self.authenticator = StandaloneAuthenticator(None) test_key = pkg_resources.resource_string( - __name__, "testdata/rsa256_key.pem") + "letsencrypt.client.tests", "testdata/rsa256_key.pem") key = le_util.Key("foo", test_key) self.key = key self.cert = achallenges.DVSNI( @@ -466,9 +474,10 @@ class DoChildProcessTest(unittest.TestCase): self.authenticator.tasks = {"abcdef.acme.invalid": self.cert} self.authenticator.parent_pid = 12345 - @mock.patch("letsencrypt.client.standalone_authenticator.socket.socket") - @mock.patch("letsencrypt.client.standalone_authenticator.os.kill") - @mock.patch("letsencrypt.client.standalone_authenticator.sys.exit") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "socket.socket") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit") def test_do_child_process_cantbind1( self, mock_exit, mock_kill, mock_socket): mock_exit.side_effect = IndentationError("subprocess would exit here") @@ -488,9 +497,10 @@ class DoChildProcessTest(unittest.TestCase): mock_exit.assert_called_once_with(1) mock_kill.assert_called_once_with(12345, signal.SIGUSR2) - @mock.patch("letsencrypt.client.standalone_authenticator.socket.socket") - @mock.patch("letsencrypt.client.standalone_authenticator.os.kill") - @mock.patch("letsencrypt.client.standalone_authenticator.sys.exit") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "socket.socket") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit") def test_do_child_process_cantbind2(self, mock_exit, mock_kill, mock_socket): mock_exit.side_effect = IndentationError("subprocess would exit here") @@ -504,7 +514,8 @@ class DoChildProcessTest(unittest.TestCase): mock_exit.assert_called_once_with(1) mock_kill.assert_called_once_with(12345, signal.SIGUSR1) - @mock.patch("letsencrypt.client.standalone_authenticator.socket.socket") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "socket.socket") def test_do_child_process_cantbind3(self, mock_socket): """Test case where attempt to bind socket results in an unhandled socket error. (The expected behavior is arguably wrong because it @@ -517,10 +528,11 @@ class DoChildProcessTest(unittest.TestCase): self.assertRaises( socket.error, self.authenticator.do_child_process, 1717, self.key) - @mock.patch("letsencrypt.client.standalone_authenticator." + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." "OpenSSL.SSL.Connection") - @mock.patch("letsencrypt.client.standalone_authenticator.socket.socket") - @mock.patch("letsencrypt.client.standalone_authenticator.os.kill") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "socket.socket") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill") def test_do_child_process_success( self, mock_kill, mock_socket, mock_connection): sample_socket = mock.MagicMock() @@ -543,7 +555,7 @@ class DoChildProcessTest(unittest.TestCase): class CleanupTest(unittest.TestCase): """Tests for cleanup() method.""" def setUp(self): - from letsencrypt.client.standalone_authenticator import \ + from letsencrypt.client.plugins.standalone.authenticator import \ StandaloneAuthenticator self.authenticator = StandaloneAuthenticator(None) self.achall = achallenges.DVSNI( @@ -552,8 +564,9 @@ class CleanupTest(unittest.TestCase): self.authenticator.tasks = {self.achall.nonce_domain: "stuff"} self.authenticator.child_pid = 12345 - @mock.patch("letsencrypt.client.standalone_authenticator.os.kill") - @mock.patch("letsencrypt.client.standalone_authenticator.time.sleep") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill") + @mock.patch("letsencrypt.client.plugins.standalone.authenticator." + "time.sleep") def test_cleanup(self, mock_sleep, mock_kill): mock_sleep.return_value = None mock_kill.return_value = None @@ -573,7 +586,7 @@ class CleanupTest(unittest.TestCase): class MoreInfoTest(unittest.TestCase): """Tests for more_info() method. (trivially)""" def setUp(self): - from letsencrypt.client.standalone_authenticator import ( + from letsencrypt.client.plugins.standalone.authenticator import ( StandaloneAuthenticator) self.authenticator = StandaloneAuthenticator(None) @@ -585,7 +598,7 @@ class MoreInfoTest(unittest.TestCase): class InitTest(unittest.TestCase): """Tests for more_info() method. (trivially)""" def setUp(self): - from letsencrypt.client.standalone_authenticator import ( + from letsencrypt.client.plugins.standalone.authenticator import ( StandaloneAuthenticator) self.authenticator = StandaloneAuthenticator(None) diff --git a/letsencrypt/client/tests/client_test.py b/letsencrypt/client/tests/client_test.py index 5ae6d6107..1c1a0d68a 100644 --- a/letsencrypt/client/tests/client_test.py +++ b/letsencrypt/client/tests/client_test.py @@ -8,8 +8,9 @@ from letsencrypt.client import errors class DetermineAuthenticatorTest(unittest.TestCase): def setUp(self): - from letsencrypt.client.apache.configurator import ApacheConfigurator - from letsencrypt.client.standalone_authenticator import ( + from letsencrypt.client.plugins.apache.configurator import ( + ApacheConfigurator) + from letsencrypt.client.plugins.standalone.authenticator import ( StandaloneAuthenticator) self.mock_stand = mock.MagicMock( @@ -65,7 +66,8 @@ class DetermineAuthenticatorTest(unittest.TestCase): class RollbackTest(unittest.TestCase): """Test the rollback function.""" def setUp(self): - from letsencrypt.client.apache.configurator import ApacheConfigurator + from letsencrypt.client.plugins.apache.configurator import ( + ApacheConfigurator) self.m_install = mock.MagicMock(spec=ApacheConfigurator) @classmethod diff --git a/letsencrypt/client/tests/revoker_test.py b/letsencrypt/client/tests/revoker_test.py index f5a940df8..ff2ce6aca 100644 --- a/letsencrypt/client/tests/revoker_test.py +++ b/letsencrypt/client/tests/revoker_test.py @@ -10,7 +10,7 @@ import mock from letsencrypt.client import errors from letsencrypt.client import le_util -from letsencrypt.client.apache import configurator +from letsencrypt.client.plugins.apache import configurator from letsencrypt.client.display import util as display_util diff --git a/setup.py b/setup.py index c07c1f2ce..ca7de3abb 100644 --- a/setup.py +++ b/setup.py @@ -96,10 +96,13 @@ setup( 'letsencrypt.acme', 'letsencrypt.acme.jose', 'letsencrypt.client', - 'letsencrypt.client.apache', 'letsencrypt.client.display', + 'letsencrypt.client.plugins', + 'letsencrypt.client.plugins.apache', + 'letsencrypt.client.plugins.apache.tests', + 'letsencrypt.client.plugins.standalone', + 'letsencrypt.client.plugins.standalone.tests', 'letsencrypt.client.tests', - 'letsencrypt.client.tests.apache', 'letsencrypt.client.tests.display', 'letsencrypt.scripts', ], @@ -120,9 +123,9 @@ setup( 'jws = letsencrypt.acme.jose.jws:CLI.run', ], 'letsencrypt.authenticators': [ - 'apache = letsencrypt.client.apache.configurator' + 'apache = letsencrypt.client.plugins.apache.configurator' ':ApacheConfigurator', - 'standalone = letsencrypt.client.standalone_authenticator' + 'standalone = letsencrypt.client.plugins.standalone.authenticator' ':StandaloneAuthenticator', ], }, From 32c33e64df284fd648c25ba74a65c8e329f11f7f Mon Sep 17 00:00:00 2001 From: James Kasten Date: Thu, 26 Mar 2015 17:39:08 -0700 Subject: [PATCH 62/92] cleanup of plugins --- .../client/plugins/apache/configurator.py | 46 ++++++------- letsencrypt/client/plugins/apache/dvsni.py | 8 +-- .../plugins/apache/tests/configurator_test.py | 6 +- .../plugins/apache/tests/parser_test.py | 2 +- .../standalone/tests/authenticator_test.py | 64 +++++++++---------- 5 files changed, 63 insertions(+), 63 deletions(-) diff --git a/letsencrypt/client/plugins/apache/configurator.py b/letsencrypt/client/plugins/apache/configurator.py index 5b682216b..fb5ba7bd1 100644 --- a/letsencrypt/client/plugins/apache/configurator.py +++ b/letsencrypt/client/plugins/apache/configurator.py @@ -165,7 +165,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): parser.case_i("SSLCertificateChainFile"), None, vhost.path) if len(path["cert_file"]) == 0 or len(path["cert_key"]) == 0: - # Throw some "can't find all of the directives error" + # Throw some can't find all of the directives error" logging.warn( "Cannot find a cert or key directive in %s", vhost.path) logging.warn("VirtualHost was not modified") @@ -224,7 +224,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.assoc[target_name] = vhost return vhost - # Check for non ssl vhosts with servernames/aliases == 'name' + # Check for non ssl vhosts with servernames/aliases == "name" for vhost in self.vhosts: if not vhost.ssl and target_name in vhost.names: vhost = self.make_vhost_ssl(vhost) @@ -288,9 +288,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): name_match = self.aug.match(("%s//*[self::directive=~regexp('%s')] | " "%s//*[self::directive=~regexp('%s')]" % (host.path, - parser.case_i('ServerName'), + parser.case_i("ServerName"), host.path, - parser.case_i('ServerAlias')))) + parser.case_i("ServerAlias")))) for name in name_match: args = self.aug.match(name + "/*") @@ -335,7 +335,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Search sites-available, httpd.conf for possible virtual hosts paths = self.aug.match( ("/files%s/sites-available//*[label()=~regexp('%s')]" % - (self.parser.root, parser.case_i('VirtualHost')))) + (self.parser.root, parser.case_i("VirtualHost")))) vhs = [] for path in paths: @@ -455,8 +455,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.reverter.register_file_creation(False, ssl_fp) try: - with open(avail_fp, 'r') as orig_file: - with open(ssl_fp, 'w') as new_file: + with open(avail_fp, "r") as orig_file: + with open(ssl_fp, "w") as new_file: new_file.write("\n") for line in orig_file: new_file.write(line) @@ -472,7 +472,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # change address to address:443 addr_match = "/files%s//* [label()=~regexp('%s')]/arg" ssl_addr_p = self.aug.match( - addr_match % (ssl_fp, parser.case_i('VirtualHost'))) + addr_match % (ssl_fp, parser.case_i("VirtualHost"))) for addr in ssl_addr_p: old_addr = obj.Addr.fromstring( @@ -483,7 +483,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Add directives vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" % - (ssl_fp, parser.case_i('VirtualHost'))) + (ssl_fp, parser.case_i("VirtualHost"))) if len(vh_p) != 1: logging.error("Error: should only be one vhost in %s", avail_fp) sys.exit(1) @@ -496,7 +496,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Log actions and create save notes logging.info("Created an SSL vhost at %s", ssl_fp) - self.save_notes += 'Created ssl vhost at %s\n' % ssl_fp + self.save_notes += "Created ssl vhost at %s\n" % ssl_fp self.save() # We know the length is one because of the assertion above @@ -597,7 +597,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.parser.add_dir(general_v.path, "RewriteEngine", "On") self.parser.add_dir(general_v.path, "RewriteRule", constants.APACHE_REWRITE_HTTPS_ARGS) - self.save_notes += ('Redirecting host in %s to ssl vhost in %s\n' % + self.save_notes += ("Redirecting host in %s to ssl vhost in %s\n" % (general_v.filep, ssl_vhost.filep)) self.save() @@ -701,7 +701,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): redirect_filename = "le-redirect-%s.conf" % ssl_vhost.names[0] redirect_filepath = os.path.join( - self.parser.root, 'sites-available', redirect_filename) + self.parser.root, "sites-available", redirect_filename) # Register the new file that will be created # Note: always register the creation before writing to ensure file will @@ -709,7 +709,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.reverter.register_file_creation(False, redirect_filepath) # Write out file - with open(redirect_filepath, 'w') as redirect_fd: + with open(redirect_filepath, "w") as redirect_fd: redirect_fd.write(redirect_file) logging.info("Created redirect file: %s", redirect_filename) @@ -719,8 +719,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.vhosts.append(new_vhost) # Finally create documentation for the change - self.save_notes += ('Created a port 80 vhost, %s, for redirection to ' - 'ssl vhost %s\n' % + self.save_notes += ("Created a port 80 vhost, %s, for redirection to " + "ssl vhost %s\n" % (new_vhost.filep, ssl_vhost.filep)) def _conflicting_host(self, ssl_vhost): @@ -877,7 +877,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): os.symlink(vhost.filep, enabled_path) vhost.enabled = True logging.info("Enabling available site: %s", vhost.filep) - self.save_notes += 'Enabled site %s\n' % vhost.filep + self.save_notes += "Enabled site %s\n" % vhost.filep return True return False @@ -899,7 +899,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: proc = subprocess.Popen( - ['sudo', self.config.apache_ctl, 'configtest'], # TODO: sudo? + ["sudo", self.config.apache_ctl, "configtest"], # TODO: sudo? stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() @@ -943,7 +943,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ try: proc = subprocess.Popen( - [self.config.apache_ctl, '-v'], + [self.config.apache_ctl, "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) text = proc.communicate()[0] @@ -958,7 +958,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): raise errors.LetsEncryptConfiguratorError( "Unable to find Apache version") - return tuple([int(i) for i in matches[0].split('.')]) + return tuple([int(i) for i in matches[0].split(".")]) def more_info(self): """Human-readable string to help understand the module""" @@ -1033,8 +1033,8 @@ def enable_mod(mod_name, apache_init_script, apache_enmod): # Use check_output so the command will finish before reloading # TODO: a2enmod is debian specific... subprocess.check_call(["sudo", apache_enmod, mod_name], # TODO: sudo? - stdout=open("/dev/null", 'w'), - stderr=open("/dev/null", 'w')) + stdout=open("/dev/null", "w"), + stderr=open("/dev/null", "w")) apache_restart(apache_init_script) except (OSError, subprocess.CalledProcessError) as err: logging.error("Error enabling mod_%s", mod_name) @@ -1056,7 +1056,7 @@ def mod_loaded(module, apache_ctl): """ try: proc = subprocess.Popen( - [apache_ctl, '-M'], + [apache_ctl, "-M"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() @@ -1094,7 +1094,7 @@ def apache_restart(apache_init_script): """ try: - proc = subprocess.Popen([apache_init_script, 'restart'], + proc = subprocess.Popen([apache_init_script, "restart"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() diff --git a/letsencrypt/client/plugins/apache/dvsni.py b/letsencrypt/client/plugins/apache/dvsni.py index 2e1c948aa..7755658e7 100644 --- a/letsencrypt/client/plugins/apache/dvsni.py +++ b/letsencrypt/client/plugins/apache/dvsni.py @@ -117,7 +117,7 @@ class ApacheDvsni(object): cert_pem, response = achall.gen_cert_and_response(s) # Write out challenge cert - with open(cert_path, 'w') as cert_chall_fd: + with open(cert_path, "w") as cert_chall_fd: cert_chall_fd.write(cert_pem) return response @@ -141,7 +141,7 @@ class ApacheDvsni(object): self.configurator.reverter.register_file_creation( True, self.challenge_conf) - with open(self.challenge_conf, 'w') as new_conf: + with open(self.challenge_conf, "w") as new_conf: new_conf.write(config_text) def _conf_include_check(self, main_config): @@ -179,13 +179,13 @@ class ApacheDvsni(object): # TODO: Python docs is not clear how mutliline string literal # newlines are parsed on different platforms. At least on # Linux (Debian sid), when source file uses CRLF, Python still - # parses it as '\n'... c.f.: + # parses it as "\n"... c.f.: # https://docs.python.org/2.7/reference/lexical_analysis.html return self.VHOST_TEMPLATE.format( vhost=ips, server_name=achall.nonce_domain, ssl_options_conf_path=self.configurator.parser.loc["ssl_options"], cert_path=self.get_cert_file(achall), key_path=achall.key.file, - document_root=document_root).replace('\n', os.linesep) + document_root=document_root).replace("\n", os.linesep) def get_cert_file(self, achall): """Returns standardized name for challenge certificate. diff --git a/letsencrypt/client/plugins/apache/tests/configurator_test.py b/letsencrypt/client/plugins/apache/tests/configurator_test.py index 0b7d4f570..91758d196 100644 --- a/letsencrypt/client/plugins/apache/tests/configurator_test.py +++ b/letsencrypt/client/plugins/apache/tests/configurator_test.py @@ -43,7 +43,7 @@ class TwoVhost80Test(util.ApacheTest): def test_get_all_names(self): names = self.config.get_all_names() self.assertEqual(names, set( - ['letsencrypt.demo', 'encryption-example.demo', 'ip-172-30-0-17'])) + ["letsencrypt.demo", "encryption-example.demo", "ip-172-30-0-17"])) def test_get_virtual_hosts(self): """Make sure all vhosts are being properly found. @@ -197,7 +197,7 @@ class TwoVhost80Test(util.ApacheTest): errors.LetsEncryptConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( - "Server Version: Apache/2.3\n Apache/2.4.7", "") + "Server Version: Apache/2.3{0} Apache/2.4.7".format(os.linesep), "") self.assertRaises( errors.LetsEncryptConfiguratorError, self.config.get_version) @@ -206,5 +206,5 @@ class TwoVhost80Test(util.ApacheTest): errors.LetsEncryptConfiguratorError, self.config.get_version) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/letsencrypt/client/plugins/apache/tests/parser_test.py b/letsencrypt/client/plugins/apache/tests/parser_test.py index d394feeaa..1696841f8 100644 --- a/letsencrypt/client/plugins/apache/tests/parser_test.py +++ b/letsencrypt/client/plugins/apache/tests/parser_test.py @@ -125,5 +125,5 @@ class ApacheParserTest(util.ApacheTest): self.assertEqual(results["default"], results["name"]) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/letsencrypt/client/plugins/standalone/tests/authenticator_test.py b/letsencrypt/client/plugins/standalone/tests/authenticator_test.py index 390d21b9f..577bc7e74 100644 --- a/letsencrypt/client/plugins/standalone/tests/authenticator_test.py +++ b/letsencrypt/client/plugins/standalone/tests/authenticator_test.py @@ -201,14 +201,14 @@ class AlreadyListeningTest(unittest.TestCase): # found to match the identified listening PID. 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)] + 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, @@ -226,12 +226,12 @@ class AlreadyListeningTest(unittest.TestCase): 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)] + 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.authenticator.already_listening(17)) @@ -247,14 +247,14 @@ class AlreadyListeningTest(unittest.TestCase): 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)] + 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.authenticator.already_listening(17) @@ -271,16 +271,16 @@ class AlreadyListeningTest(unittest.TestCase): 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)] + 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.authenticator.already_listening(12345) From ffff84ee55f33122434c214af5e4d7e74ad65efb Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 27 Mar 2015 08:43:05 +0000 Subject: [PATCH 63/92] 100% coverage for acme.fields --- letsencrypt/acme/fields.py | 8 +++++++- letsencrypt/acme/fields_test.py | 35 +++++++++++++++++++++++++++++++++ setup.py | 1 + 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 letsencrypt/acme/fields_test.py diff --git a/letsencrypt/acme/fields.py b/letsencrypt/acme/fields.py index 59a72953b..f001f1cd5 100644 --- a/letsencrypt/acme/fields.py +++ b/letsencrypt/acme/fields.py @@ -5,7 +5,13 @@ from letsencrypt.acme import jose class RFC3339Field(jose.Field): - """RFC3339 field encoder/decoder""" + """RFC3339 field encoder/decoder. + + Handles decoding/encoding between RFC3339 strings and aware (not + naive) `datetime.datetime` objects + (e.g. ``datetime.datetime.now(pytz.utc)``). + + """ @classmethod def default_encoder(cls, value): diff --git a/letsencrypt/acme/fields_test.py b/letsencrypt/acme/fields_test.py new file mode 100644 index 000000000..204849408 --- /dev/null +++ b/letsencrypt/acme/fields_test.py @@ -0,0 +1,35 @@ +"""Tests for letsencrypt.acme.fields.""" +import datetime +import unittest + +import pytz + +from letsencrypt.acme import jose + + +class RFC3339FieldTest(unittest.TestCase): + """Tests for letsencrypt.acme.fields.RFC3339Field.""" + + def setUp(self): + self.decoded = datetime.datetime(2015, 3, 27, tzinfo=pytz.utc) + self.encoded = '2015-03-27T00:00:00Z' + + def test_default_encoder(self): + from letsencrypt.acme.fields import RFC3339Field + self.assertEqual( + self.encoded, RFC3339Field.default_encoder(self.decoded)) + + def test_default_encoder_naive_fails(self): + from letsencrypt.acme.fields import RFC3339Field + self.assertRaises( + ValueError, RFC3339Field.default_encoder, datetime.datetime.now()) + + def test_default_decoder(self): + from letsencrypt.acme.fields import RFC3339Field + self.assertEqual( + self.decoded, RFC3339Field.default_decoder(self.encoded)) + + def test_default_decoder_raises_deserialization_error(self): + from letsencrypt.acme.fields import RFC3339Field + self.assertRaises( + jose.DeserializationError, RFC3339Field.default_decoder, '') diff --git a/setup.py b/setup.py index b25b7fdb4..b70bfa031 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ install_requires = [ 'pyrfc3339', 'python-augeas', 'python2-pythondialog', + 'pytz', 'requests', 'werkzeug', 'zope.component', From b12e4ba3572644748d7fb72013923d7b88fcbf83 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 27 Mar 2015 08:47:14 +0000 Subject: [PATCH 64/92] ImmutableMap.update: test, lint --- letsencrypt/acme/jose/util.py | 2 +- letsencrypt/acme/jose/util_test.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/letsencrypt/acme/jose/util.py b/letsencrypt/acme/jose/util.py index e8d2a17a6..0aa5c271c 100644 --- a/letsencrypt/acme/jose/util.py +++ b/letsencrypt/acme/jose/util.py @@ -61,7 +61,7 @@ class ImmutableMap(collections.Mapping, collections.Hashable): """Return updated map.""" items = dict(self) items.update(kwargs) - return type(self)(**items) + return type(self)(**items) # pylint: disable=star-args def __getitem__(self, key): try: diff --git a/letsencrypt/acme/jose/util_test.py b/letsencrypt/acme/jose/util_test.py index 671b45472..8d88d8b7e 100644 --- a/letsencrypt/acme/jose/util_test.py +++ b/letsencrypt/acme/jose/util_test.py @@ -25,6 +25,10 @@ class ImmutableMapTest(unittest.TestCase): self.a2 = self.A(x=3, y=4) self.b = self.B(x=1, y=2) + def test_update(self): + self.assertEqual(self.A(x=2, y=2), self.a1.update(x=2)) + self.assertEqual(self.a2, self.a1.update(x=3, y=4)) + def test_get_missing_item_raises_key_error(self): self.assertRaises(KeyError, self.a1.__getitem__, 'z') From c985a8987b85d8947b1bb91ec92f27f03c7d26b8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 27 Mar 2015 09:47:31 +0000 Subject: [PATCH 65/92] Add fields.rst docs --- docs/api/acme/index.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/api/acme/index.rst b/docs/api/acme/index.rst index 3f4a8f6ea..9eb93ec6c 100644 --- a/docs/api/acme/index.rst +++ b/docs/api/acme/index.rst @@ -30,10 +30,18 @@ Challenges Other ACME objects ------------------ + .. automodule:: letsencrypt.acme.other :members: +Fields +------ + +.. automodule:: letsencrypt.acme.fields + :members: + + Errors ------ From 3762622ee925cff795a1907982a88a4af6471275 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 27 Mar 2015 09:47:03 +0000 Subject: [PATCH 66/92] Tests, lint, and docs for messages2 --- letsencrypt/acme/messages2.py | 87 +++++++++------ letsencrypt/acme/messages2_test.py | 172 +++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+), 34 deletions(-) create mode 100644 letsencrypt/acme/messages2_test.py diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 0fbb605d0..49ca24e73 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -13,31 +13,37 @@ class Error(jose.JSONObjectWithFields, Exception): ERROR_TYPE_NAMESPACE = 'urn:acme:error:' ERROR_TYPE_DESCRIPTIONS = { - "malformed": "The request message was malformed", - "unauthorized": "The client lacks sufficient authorization", - "serverInternal": "The server experienced an internal error", - "badCSR": "The CSR is unacceptable (e.g., due to a short key)", + 'malformed': 'The request message was malformed', + 'unauthorized': 'The client lacks sufficient authorization', + 'serverInternal': 'The server experienced an internal error', + 'badCSR': 'The CSR is unacceptable (e.g., due to a short key)', } - typ = jose.Field('type', omitempty=True) # Boulder omits, spec requires + # TODO: Boulder omits 'type' and 'instance', spec requires + typ = jose.Field('type', omitempty=True) title = jose.Field('title', omitempty=True) detail = jose.Field('detail') - # Boulder omits, spec requires instance = jose.Field('instance', omitempty=True) @typ.encoder - def typ(value): - return ERROR_TYPE_NAMESPACE + value + def typ(value): # pylint: disable=missing-docstring,no-self-argument + return Error.ERROR_TYPE_NAMESPACE + value @typ.decoder - def typ(value): - if not value.startswith(ERROR_TYPE_NAMESPACE): - raise jose.DeserializationError('Unrecognized error type') + 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') - return value[len(ERROR_TYPE_NAMESPACE):] + 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): + def description(self): # pylint: disable=missing-docstring,no-self-argument return self.ERROR_TYPE_DESCRIPTIONS[self.typ] @@ -61,7 +67,7 @@ class _Constant(jose.JSONDeSerializable): return cls.POSSIBLE_NAMES[value] def __repr__(self): - return '{0}({0})'.format(self.__class__.__name__, self.name) + return '{0}({1})'.format(self.__class__.__name__, self.name) def __eq__(self, other): return isinstance(other, type(self)) and other.name == self.name @@ -131,26 +137,32 @@ class Registration(ResourceBody): class ChallengeResource(Resource, jose.JSONObjectWithFields): """Challenge resource. - :ivar body: `.challenges.Challenge` + :ivar body: `.challenges.ChallengeBody` :ivar authz_uri: URI found in the 'up' Link header. """ __slots__ = ('body', 'authz_uri') @property - def uri(self): - return body.uri + def uri(self): # pylint: disable=missing-docstring,no-self-argument + # bug? 'method already defined line None' + # pylint: disable=function-redefined + return self.body.uri -class Challenge(ResourceBody): +class ChallengeBody(ResourceBody): """Challenge resource body. + Confusingly, this has a similar name to `.challenges.Challenge`, as + well as `.achallanges.AnnotatedChallenge` or + `.achallanges.IndexedChallenge`. Use names such as ``challb`` to + distinguish instances of this class from ``achall`` or ``ichall``. + .. todo:: - Confusingly, this has the same name as - `challenges.Challenge`. Indeed, this class could be integrated - with challenges.Challenge, but this way it would be confusing - when compared to acme-spec, where all challenges are presented - without 'uri', 'status', or 'validated' fields. + This class could be integrated with challenges.Challenge, but + this way it would be confusing when compared to acme-spec, where + all challenges are presented without 'uri', 'status', or + 'validated' fields. """ @@ -160,15 +172,15 @@ class Challenge(ResourceBody): validated = fields.RFC3339Field('validated', omitempty=True) def to_json(self): - jobj = super(Challenge, self).to_json() + jobj = super(ChallengeBody, self).to_json() jobj.update(self.chall.to_json()) return jobj @classmethod def fields_from_json(cls, jobj): - fields = super(Challenge, cls).fields_from_json(jobj) - fields['chall'] = challenges.Challenge.from_json(jobj) - return fields + jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj) + jobj_fields['chall'] = challenges.Challenge.from_json(jobj) + return jobj_fields class AuthorizationResource(Resource): @@ -206,7 +218,8 @@ class Authorization(ResourceBody): @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument return tuple( - ChallengeResource(body=Challenge.from_json(chall), authz_uri=None) + ChallengeResource( + body=ChallengeBody.from_json(chall), authz_uri=None) for chall in value) @property @@ -238,23 +251,29 @@ class CertificateResource(Resource): class Revocation(jose.JSONObjectWithFields): - """Revocation message.""" + """Revocation message. + + :ivar revoke: Either a `datetime.datetime` or `NOW`. + + """ NOW = 'now' + """A possible value for `revoke`, denoting that certificate should + be revoked now.""" revoke = jose.Field('revoke') authorizations = CertificateRequest._fields['authorizations'] @revoke.decoder - def revoke(value): - if jobj == NOW: - return jobj + def revoke(value): # pylint: disable=missing-docstring,no-self-argument + if value == Revocation.NOW: + return value else: return fields.RFC3339Field.default_decoder(value) @revoke.encoder - def revoke(value): - if jobj == NOW: + def revoke(value): # pylint: disable=missing-docstring,no-self-argument + if value == Revocation.NOW: return value else: return fields.RFC3339Field.default_encoder(value) diff --git a/letsencrypt/acme/messages2_test.py b/letsencrypt/acme/messages2_test.py new file mode 100644 index 000000000..3d94e2bf2 --- /dev/null +++ b/letsencrypt/acme/messages2_test.py @@ -0,0 +1,172 @@ +"""Tests for letsencrypt.acme.messages2.""" +import datetime +import unittest + +import mock +import pytz + +from letsencrypt.acme import challenges +from letsencrypt.acme import jose + + +class ErrorTest(unittest.TestCase): + """Tests for letsencrypt.acme.messages2.Error.""" + + def setUp(self): + from letsencrypt.acme.messages2 import Error + self.error = Error(detail='foo', typ='malformed') + + def test_typ_prefix(self): + self.assertEqual('malformed', self.error.typ) + self.assertEqual( + 'urn:acme:error:malformed', self.error.to_json()['type']) + self.assertEqual( + 'malformed', self.error.from_json(self.error.to_json()).typ) + + def test_typ_decoder_missing_prefix(self): + from letsencrypt.acme.messages2 import Error + self.assertRaises(jose.DeserializationError, Error.from_json, + {'detail': 'foo', 'type': 'malformed'}) + self.assertRaises(jose.DeserializationError, Error.from_json, + {'detail': 'foo', 'type': 'not valid bare type'}) + + def test_typ_decoder_not_recognized(self): + from letsencrypt.acme.messages2 import Error + self.assertRaises(jose.DeserializationError, Error.from_json, + {'detail': 'foo', 'type': 'urn:acme:error:baz'}) + + def test_description(self): + self.assertEqual( + 'The request message was malformed', self.error.description) + + +class ConstantTest(unittest.TestCase): + """Tests for letsencrypt.acme.messages2._Constant.""" + + def setUp(self): + from letsencrypt.acme.messages2 import _Constant + class MockConstant(_Constant): # pylint: disable=missing-docstring + POSSIBLE_NAMES = {} + + self.MockConstant = MockConstant # pylint: disable=invalid-name + self.const_a = MockConstant('a') + self.const_b = MockConstant('b') + + def test_to_json(self): + self.assertEqual('a', self.const_a.to_json()) + self.assertEqual('b', self.const_b.to_json()) + + def test_from_json(self): + self.assertEqual(self.const_a, self.MockConstant.from_json('a')) + self.assertRaises( + jose.DeserializationError, self.MockConstant.from_json, 'c') + + def test_repr(self): + self.assertEqual('MockConstant(a)', repr(self.const_a)) + self.assertEqual('MockConstant(b)', repr(self.const_b)) + + +class ChallengeResourceTest(unittest.TestCase): + """Tests for letsencrypt.acme.messages2.ChallengeResource.""" + + def test_uri(self): + from letsencrypt.acme.messages2 import ChallengeResource + self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock( + uri='http://challb'), authz_uri='http://authz').uri) + + +class ChallengeBodyTest(unittest.TestCase): + """Tests for letsencrypt.acme.messages2.ChallengeBody.""" + + def setUp(self): + self.chall = challenges.DNS(token='foo') + + from letsencrypt.acme.messages2 import ChallengeBody + from letsencrypt.acme.messages2 import STATUS_VALID + self.status = STATUS_VALID + self.challb = ChallengeBody( + uri='http://challb', chall=self.chall, status=self.status) + + self.jobj_to = { + 'uri': 'http://challb', + 'status': self.status, + 'type': 'dns', + 'token': 'foo', + } + self.jobj_from = self.jobj_to.copy() + self.jobj_from['status'] = 'valid' + + def test_to_json(self): + self.assertEqual(self.jobj_to, self.challb.to_json()) + + def test_fields_from_json(self): + from letsencrypt.acme.messages2 import ChallengeBody + self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from)) + + +class AuthorizationTest(unittest.TestCase): + """Tests for letsencrypt.acme.messages2.Authorization.""" + + def setUp(self): + from letsencrypt.acme.messages2 import ChallengeBody + from letsencrypt.acme.messages2 import STATUS_VALID + self.challbs = ( + ChallengeBody( + uri='http://challb1', status=STATUS_VALID, + chall=challenges.SimpleHTTPS(token='IlirfxKKXAsHtmzK29Pj8A')), + ChallengeBody(uri='http://challb2', status=STATUS_VALID, + chall=challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA')), + ChallengeBody(uri='http://challb3', status=STATUS_VALID, + chall=challenges.RecoveryToken()), + ) + combinations = ((0, 2), (1, 2)) + + from letsencrypt.acme.messages2 import Authorization + from letsencrypt.acme.messages2 import Identifier + from letsencrypt.acme.messages2 import IDENTIFIER_FQDN + identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') + self.authz = Authorization( + identifier=identifier, combinations=combinations, + challenges=self.challbs) + + self.jobj_from = { + 'identifier': identifier.fully_serialize(), + 'challenges': [challb.fully_serialize() for challb in self.challbs], + 'combinations': combinations, + } + + def test_from_json(self): + from letsencrypt.acme.messages2 import Authorization + Authorization.from_json(self.jobj_from) + + def test_resolved_combinations(self): + self.assertEqual(self.authz.resolved_combinations, ( + (self.challbs[0], self.challbs[2]), + (self.challbs[1], self.challbs[2]), + )) + + +class RevocationTest(unittest.TestCase): + """Tests for letsencrypt.acme.messages2.RevocationTest.""" + + def setUp(self): + from letsencrypt.acme.messages2 import Revocation + self.rev_now = Revocation(authorizations=(), revoke=Revocation.NOW) + self.rev_date = Revocation(authorizations=(), revoke=datetime.datetime( + 2015, 3, 27, tzinfo=pytz.utc)) + self.jobj_now = {'authorizations': (), 'revoke': Revocation.NOW} + self.jobj_date = {'authorizations': (), + 'revoke': '2015-03-27T00:00:00Z'} + + def test_revoke_decoder(self): + from letsencrypt.acme.messages2 import Revocation + self.assertEqual(self.rev_now, Revocation.from_json(self.jobj_now)) + self.assertEqual(self.rev_date, Revocation.from_json(self.jobj_date)) + + def test_revoke_encoder(self): + self.assertEqual(self.jobj_now, self.rev_now.to_json()) + self.assertEqual(self.jobj_date, self.rev_date.to_json()) + + +if __name__ == '__main__': + unittest.main() From 8fa2204afe8b6ee6fe758f51f438495ca17e0659 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 27 Mar 2015 12:29:27 -0700 Subject: [PATCH 67/92] Add disclaimer in plugins doc --- docs/plugins.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/plugins.rst b/docs/plugins.rst index fafb8d5d3..0451bfe3f 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -5,9 +5,15 @@ Plugins Let's Encrypt client supports dynamic discovery of plugins through the `setuptools entry points`_. This way you can, for example, create a custom implementation of -`~letsencrypt.client.interfaces.IAuthenticator` without having to -merge it with the core upstream source code. Example is provided in +`~letsencrypt.client.interfaces.IAuthenticator` or the +'~letsencrypt.client.interfaces.IInstaller' without having to +merge it with the core upstream source code. An example is provided in ``examples/plugins/`` directory. +Please be aware though that as this client is still in a developer-preview +stage, the API may undergo a few changes. If you believe the plugin will be +beneficial to the community, please consider submitting a pull request to the +repo and we will update it with any necessary API changes. + .. _`setuptools entry points`: https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins From 0a1687eed5e8ad79c12399de0a9426be2c0871ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=A4rtner?= Date: Fri, 27 Mar 2015 20:31:29 +0100 Subject: [PATCH 68/92] Fixed wrong linking to CONTRIBUTING --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 86d85ed1d..b65230dc4 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ Documentation: https://letsencrypt.readthedocs.org/ Software project: https://github.com/letsencrypt/lets-encrypt-preview -Notes for developers: CONTRIBUTING.rst_ +Notes for developers: CONTRIBUTING.md_ Main Website: https://letsencrypt.org/ From 5763da07ff0a88354d131a297801100e9a1db81e Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 27 Mar 2015 12:38:46 -0700 Subject: [PATCH 69/92] Finish contributing.md update Fix relevant links in README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b65230dc4..fac36dbd7 100644 --- a/README.rst +++ b/README.rst @@ -91,4 +91,4 @@ email to client-dev+subscribe@letsencrypt.org) .. _Freenode: https://freenode.net .. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev -.. _CONTRIBUTING.rst: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/CONTRIBUTING.rst +.. _CONTRIBUTING.md: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/CONTRIBUTING.md From 1349b5241cfbf8e32ce1562712d6cdc314351ffc Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 27 Mar 2015 19:55:26 +0000 Subject: [PATCH 70/92] More sensible full serialization --- letsencrypt/acme/jose/interfaces.py | 30 ++++++++++++++---------- letsencrypt/acme/jose/interfaces_test.py | 9 +++++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/letsencrypt/acme/jose/interfaces.py b/letsencrypt/acme/jose/interfaces.py index 446a5d2b0..285f51747 100644 --- a/letsencrypt/acme/jose/interfaces.py +++ b/letsencrypt/acme/jose/interfaces.py @@ -129,18 +129,24 @@ class JSONDeSerializable(object): :returns: Fully serialized object. """ - partial = self.to_json() - try_serialize = (lambda x: x.fully_serialize() - if isinstance(x, JSONDeSerializable) else x) - if isinstance(partial, basestring): # strings are sequences - return partial - if isinstance(partial, collections.Sequence): - return [try_serialize(elem) for elem in partial] - elif isinstance(partial, collections.Mapping): - return dict([(try_serialize(key), try_serialize(value)) - for key, value in partial.iteritems()]) - else: - return partial + def _serialize(obj): + if isinstance(obj, JSONDeSerializable): + return _serialize(obj.to_json()) + if isinstance(obj, basestring): # strings are sequence + return obj + elif isinstance(obj, list): + return [_serialize(subobj) for subobj in obj] + elif isinstance(obj, collections.Sequence): + # default to tuple, otherwise Mapping could get + # unhashable list + return tuple(_serialize(subobj) for subobj in obj) + elif isinstance(obj, collections.Mapping): + return dict((_serialize(key), _serialize(value)) + for key, value in obj.iteritems()) + else: + return obj + + return _serialize(self) @util.abstractclassmethod def from_json(cls, unused_jobj): diff --git a/letsencrypt/acme/jose/interfaces_test.py b/letsencrypt/acme/jose/interfaces_test.py index 2e5606bce..90e34d66d 100644 --- a/letsencrypt/acme/jose/interfaces_test.py +++ b/letsencrypt/acme/jose/interfaces_test.py @@ -3,6 +3,7 @@ import unittest class JSONDeSerializableTest(unittest.TestCase): + # pylint: disable=too-many-instance-attributes def setUp(self): from letsencrypt.acme.jose.interfaces import JSONDeSerializable @@ -50,6 +51,8 @@ class JSONDeSerializableTest(unittest.TestCase): self.basic2 = Basic('foo2') self.seq = Sequence(self.basic1, self.basic2) self.mapping = Mapping(self.basic1, self.basic2) + self.nested = Basic([[self.basic1]]) + self.tuple = Basic(('foo',)) # pylint: disable=invalid-name self.Basic = Basic @@ -66,6 +69,12 @@ class JSONDeSerializableTest(unittest.TestCase): mock_value = object() self.assertTrue(self.Basic(mock_value).fully_serialize() is mock_value) + def test_fully_serialize_nested(self): + self.assertEqual(self.nested.fully_serialize(), [['foo1']]) + + def test_fully_serialize(self): + self.assertEqual(self.tuple.fully_serialize(), (('foo', ))) + def test_from_json_not_implemented(self): from letsencrypt.acme.jose.interfaces import JSONDeSerializable self.assertRaises(TypeError, JSONDeSerializable.from_json, 'xxx') From 989b8f059b4e6f6ad4267e1435043bae935750db Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 27 Mar 2015 15:20:43 -0700 Subject: [PATCH 71/92] Update documentation --- docs/api/client/apache.rst | 29 ------------ docs/api/client/plugins/apache.rst | 29 ++++++++++++ docs/api/client/plugins/standalone.rst | 11 +++++ docs/api/client/standalone_authenticator.rst | 5 -- .../plugins/letsencrypt_example_plugins.py | 2 + .../client/plugins/apache/configurator.py | 47 +++++++++++-------- 6 files changed, 69 insertions(+), 54 deletions(-) delete mode 100644 docs/api/client/apache.rst create mode 100644 docs/api/client/plugins/apache.rst create mode 100644 docs/api/client/plugins/standalone.rst delete mode 100644 docs/api/client/standalone_authenticator.rst diff --git a/docs/api/client/apache.rst b/docs/api/client/apache.rst deleted file mode 100644 index e69826cf9..000000000 --- a/docs/api/client/apache.rst +++ /dev/null @@ -1,29 +0,0 @@ -:mod:`letsencrypt.client.apache` --------------------------------- - -.. automodule:: letsencrypt.client.apache - :members: - -:mod:`letsencrypt.client.apache.configurator` -============================================= - -.. automodule:: letsencrypt.client.apache.configurator - :members: - -:mod:`letsencrypt.client.apache.dvsni` -============================================= - -.. automodule:: letsencrypt.client.apache.dvsni - :members: - -:mod:`letsencrypt.client.apache.obj` -==================================== - -.. automodule:: letsencrypt.client.apache.obj - :members: - -:mod:`letsencrypt.client.apache.parser` -======================================= - -.. automodule:: letsencrypt.client.apache.parser - :members: diff --git a/docs/api/client/plugins/apache.rst b/docs/api/client/plugins/apache.rst new file mode 100644 index 000000000..6e6e6c462 --- /dev/null +++ b/docs/api/client/plugins/apache.rst @@ -0,0 +1,29 @@ +:mod:`letsencrypt.client.plugins.apache` +---------------------------------------- + +.. automodule:: letsencrypt.client.plugins.apache + :members: + +:mod:`letsencrypt.client.plugins.apache.configurator` +===================================================== + +.. automodule:: letsencrypt.client.plugins.apache.configurator + :members: + +:mod:`letsencrypt.client.plugins.apache.dvsni` +============================================== + +.. automodule:: letsencrypt.client.plugins.apache.dvsni + :members: + +:mod:`letsencrypt.client.plugins.apache.obj` +============================================ + +.. automodule:: letsencrypt.client.plugins.apache.obj + :members: + +:mod:`letsencrypt.client.plugins.apache.parser` +=============================================== + +.. automodule:: letsencrypt.client.plugins.apache.parser + :members: diff --git a/docs/api/client/plugins/standalone.rst b/docs/api/client/plugins/standalone.rst new file mode 100644 index 000000000..44cf4b8ca --- /dev/null +++ b/docs/api/client/plugins/standalone.rst @@ -0,0 +1,11 @@ +:mod:`letsencrypt.client.plugins.standalone` +-------------------------------------------- + +.. automodule:: letsencrypt.client.plugins.standalone + :members: + +:mod:`letsencrypt.client.plugins.standalone.authenticator` +========================================================== + +.. automodule:: letsencrypt.client.plugins.standalone.authenticator + :members: diff --git a/docs/api/client/standalone_authenticator.rst b/docs/api/client/standalone_authenticator.rst deleted file mode 100644 index d05f4f057..000000000 --- a/docs/api/client/standalone_authenticator.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.client.standalone_authenticator` --------------------------------------------------- - -.. automodule:: letsencrypt.client.standalone_authenticator - :members: diff --git a/examples/plugins/letsencrypt_example_plugins.py b/examples/plugins/letsencrypt_example_plugins.py index 6817c7f1d..987a2b33b 100644 --- a/examples/plugins/letsencrypt_example_plugins.py +++ b/examples/plugins/letsencrypt_example_plugins.py @@ -14,3 +14,5 @@ class Authenticator(object): # Implement all methods from IAuthenticator, remembering to add # "self" as first argument, e.g. def prepare(self)... + + # For full examples, see letsencrypt.client.plugins diff --git a/letsencrypt/client/plugins/apache/configurator.py b/letsencrypt/client/plugins/apache/configurator.py index fb5ba7bd1..028c32bbb 100644 --- a/letsencrypt/client/plugins/apache/configurator.py +++ b/letsencrypt/client/plugins/apache/configurator.py @@ -68,12 +68,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :type config: :class:`~letsencrypt.client.interfaces.IConfig` :ivar parser: Handles low level parsing - :type parser: :class:`letsencrypt.client.plugins.apache.parser` + :type parser: :class:`~letsencrypt.client.plugins.apache.parser` :ivar tup version: version of Apache :ivar list vhosts: All vhosts found in the configuration (:class:`list` of - :class:`letsencrypt.client.plugins.apache.obj.VirtualHost`) + :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`) :ivar dict assoc: Mapping between domains and vhosts @@ -204,7 +204,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str target_name: domain name :returns: ssl vhost associated with name - :rtype: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` + :rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` """ # Allows for domain names to be associated with a virtual host @@ -245,7 +245,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str domain: domain name to associate :param vhost: virtual host to associate with domain - :type vhost: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` + :type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` """ self.assoc[domain] = vhost @@ -282,7 +282,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Helper function for get_virtual_hosts(). :param host: In progress vhost whose names will be added - :type host: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` + :type host: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` """ name_match = self.aug.match(("%s//*[self::directive=~regexp('%s')] | " @@ -303,7 +303,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str path: Augeas path to virtual host :returns: newly created vhost - :rtype: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` + :rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` """ addrs = set() @@ -327,7 +327,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Returns list of virtual hosts found in the Apache configuration. :returns: List of - :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` objects + :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` objects found in configuration :rtype: list @@ -405,7 +405,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Checks to see if the server is ready for SNI challenges. :param vhost: VirtualHost to check SNI compatibility - :type vhost: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` + :type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` :param str default_addr: TODO - investigate function further @@ -437,10 +437,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): .. note:: This function saves the configuration :param nonssl_vhost: Valid VH that doesn't have SSLEngine on - :type nonssl_vhost: :class:`~apache.obj.VirtualHost` + :type nonssl_vhost: + :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` :returns: SSL vhost - :rtype: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` + :rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` """ avail_fp = nonssl_vhost.filep @@ -560,13 +561,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): .. note:: This function saves the configuration :param ssl_vhost: Destination of traffic, an ssl enabled vhost - :type ssl_vhost: :class:`~apache.obj.VirtualHost` + :type ssl_vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` :param unused_options: Not currently used :type unused_options: Not Available :returns: Success, general_vhost (HTTP vhost) - :rtype: (bool, :class:`~apache.obj.VirtualHost`) + :rtype: (bool, :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`) """ if not mod_loaded("rewrite_module", self.config.apache_ctl): @@ -618,7 +619,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): -1 is also returned in case of no redirection/rewrite directives :param vhost: vhost to check - :type vhost: :class:`letsencrypt.client.plugins.apache.obj.VirtualHost` + :type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` :returns: Success, code value... see documentation :rtype: bool, int @@ -650,10 +651,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Creates an http_vhost specifically to redirect for the ssl_vhost. :param ssl_vhost: ssl vhost - :type ssl_vhost: :class:`~apache.obj.VirtualHost` + :type ssl_vhost: + :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` - :returns: Success, vhost - :rtype: (bool, :class:`~apache.obj.VirtualHost`) + :returns: tuple of the form + (`success`, + :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`) + :rtype: tuple """ # Consider changing this to a dictionary check @@ -735,7 +739,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not conflict: returns space separated list of new host addrs :param ssl_vhost: SSL Vhost to check for possible port 80 redirection - :type ssl_vhost: :class:`~apache.obj.VirtualHost` + :type ssl_vhost: + :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` :returns: TODO :rtype: TODO @@ -768,10 +773,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): Consider changing this into a dict check :param ssl_vhost: ssl vhost to check - :type ssl_vhost: :class:`~apache.obj.VirtualHost` + :type ssl_vhost: + :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` :returns: HTTP vhost or None if unsuccessful - :rtype: :class:`~apache.obj.VirtualHost` or None + :rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` + or None """ # _default_:443 check @@ -861,7 +868,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): .. todo:: Make sure link is not broken... :param vhost: vhost to enable - :type vhost: :class:`~apache.obj.VirtualHost` + :type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` :returns: Success :rtype: bool From fa79e3c5ef411b04a5d59e07051e4a3b93c16c3c Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 27 Mar 2015 15:37:34 -0700 Subject: [PATCH 72/92] fix pylint >80 character errors --- letsencrypt/client/plugins/apache/configurator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/letsencrypt/client/plugins/apache/configurator.py b/letsencrypt/client/plugins/apache/configurator.py index 028c32bbb..e6104a559 100644 --- a/letsencrypt/client/plugins/apache/configurator.py +++ b/letsencrypt/client/plugins/apache/configurator.py @@ -561,13 +561,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): .. note:: This function saves the configuration :param ssl_vhost: Destination of traffic, an ssl enabled vhost - :type ssl_vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` + :type ssl_vhost: + :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` :param unused_options: Not currently used :type unused_options: Not Available :returns: Success, general_vhost (HTTP vhost) - :rtype: (bool, :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`) + :rtype: (bool, + :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`) """ if not mod_loaded("rewrite_module", self.config.apache_ctl): From fadad74d480b8bf83aad6e84870a87997f2a536f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 27 Mar 2015 10:33:07 +0000 Subject: [PATCH 73/92] Test, lint, and docs for network2 --- letsencrypt/acme/messages2.py | 17 +- letsencrypt/acme/messages2_test.py | 2 +- letsencrypt/client/network2.py | 201 ++++++---- letsencrypt/client/tests/network2_test.py | 453 ++++++++++++++++++++++ 4 files changed, 595 insertions(+), 78 deletions(-) create mode 100644 letsencrypt/client/tests/network2_test.py diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 49ca24e73..f3ce53665 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -117,10 +117,10 @@ class RegistrationResource(Resource): :ivar body: `Registration` :ivar str uri: URI of the resource. - :ivar new_authz_uri: URI found in the 'next' Link header + :ivar new_authzr_uri: URI found in the 'next' Link header """ - __slots__ = ('body', 'uri', 'new_authz_uri', 'terms_of_service') + __slots__ = ('body', 'uri', 'new_authzr_uri', 'terms_of_service') class Registration(ResourceBody): @@ -138,10 +138,10 @@ class ChallengeResource(Resource, jose.JSONObjectWithFields): """Challenge resource. :ivar body: `.challenges.ChallengeBody` - :ivar authz_uri: URI found in the 'up' Link header. + :ivar authzr_uri: URI found in the 'up' Link header. """ - __slots__ = ('body', 'authz_uri') + __slots__ = ('body', 'authzr_uri') @property def uri(self): # pylint: disable=missing-docstring,no-self-argument @@ -217,10 +217,7 @@ class Authorization(ResourceBody): @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument - return tuple( - ChallengeResource( - body=ChallengeBody.from_json(chall), authz_uri=None) - for chall in value) + return tuple(ChallengeBody.from_json(chall) for chall in value) @property def resolved_combinations(self): @@ -232,7 +229,7 @@ class Authorization(ResourceBody): class CertificateRequest(jose.JSONObjectWithFields): """ACME new-cert request. - :ivar csr: `M2Crypto.X509.Request` + :ivar csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` """ csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) @@ -242,7 +239,7 @@ class CertificateRequest(jose.JSONObjectWithFields): class CertificateResource(Resource): """Authorization resource. - :ivar body: `M2Crypto.X509.X509` + :ivar body: `M2Crypto.X509.X509` wrapped in `.ComparableX509` :ivar cert_chain_uri: URI found in the 'up' Link header :ivar authzrs: `list` of `AuthorizationResource`. diff --git a/letsencrypt/acme/messages2_test.py b/letsencrypt/acme/messages2_test.py index 3d94e2bf2..5297d6362 100644 --- a/letsencrypt/acme/messages2_test.py +++ b/letsencrypt/acme/messages2_test.py @@ -72,7 +72,7 @@ class ChallengeResourceTest(unittest.TestCase): def test_uri(self): from letsencrypt.acme.messages2 import ChallengeResource self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock( - uri='http://challb'), authz_uri='http://authz').uri) + uri='http://challb'), authzr_uri='http://authz').uri) class ChallengeBodyTest(unittest.TestCase): diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index c1789808d..13c3e8149 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -6,12 +6,10 @@ import itertools import logging import time +import M2Crypto import requests import werkzeug -import M2Crypto - -from letsencrypt.acme import challenges from letsencrypt.acme import jose from letsencrypt.acme import messages2 @@ -40,9 +38,13 @@ class Network(object): self.key = key self.alg = alg - def _wrap_in_jws(self, data): - """Wrap `JSONDeSerializable` object in JWS.""" - dumps = data.json_dumps() + def _wrap_in_jws(self, obj): + """Wrap `JSONDeSerializable` object in JWS. + + :rtype: `.JWS` + + """ + dumps = obj.json_dumps() logging.debug('Serialized JSON: %s', dumps) return jose.JWS.sign( payload=dumps, key=self.key, alg=self.alg).json_dumps() @@ -52,11 +54,12 @@ class Network(object): """Check response content and its type. .. note:: - Checking is not strict: skips wrong server response Content-Type - if response is an expected JSON object (c.f. Boulder #56). + Checking is not strict: wrong server response ``Content-Type`` + HTTP header is ignored if response is an expected JSON object + (c.f. Boulder #56). """ - response_ct = response.headers['content-type'] + response_ct = response.headers.get('Content-Type') try: # TODO: response.json() is called twice, once here, and @@ -81,15 +84,12 @@ class Network(object): # response is not JSON object raise errors.NetworkError(response) else: - if jobj is not None and ( - response_ct != cls.JSON_CONTENT_TYPE or - response_ct != cls.JSON_ERROR_CONTENT_TYPE): + if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: logging.debug( 'Ignoring wrong Content-Type (%r) for JSON decodable ' 'response', response_ct) - if (content_type is not None and response_ct != content_type - and content_type != cls.JSON_CONTENT_TYPE): + if content_type == cls.JSON_CONTENT_TYPE and jobj is None: raise errors.NetworkError( 'Unexpected response Content-Type: {0}'.format(response_ct)) @@ -106,13 +106,13 @@ class Network(object): response = requests.get(uri, **kwargs) except requests.exceptions.RequestException as error: raise errors.NetworkError(error) - self._check_response(response, content_type) + self._check_response(response, content_type=content_type) return response def _post(self, uri, data, content_type=JSON_CONTENT_TYPE, **kwargs): """Send POST data. - :param str content_type: Expected Content-Type, fails if not set. + :param str content_type: Expected ``Content-Type``, fails if not set. :raises letsencrypt.acme.messages2.NetworkError: @@ -127,31 +127,35 @@ class Network(object): raise errors.NetworkError(error) logging.debug('Received response %s: %s', response, response.text) - self._check_response(response, content_type) + self._check_response(response, content_type=content_type) return response @classmethod - def _regr_from_response(cls, response, uri=None, new_authz_uri=None): + def _regr_from_response(cls, response, uri=None, new_authzr_uri=None, + terms_of_service=None): terms_of_service = ( - response.links['next']['url'] - if 'terms-of-service' in response.links else None) + response.links['terms-of-service']['url'] + if 'terms-of-service' in response.links else terms_of_service) - if new_authz_uri is None: + if new_authzr_uri is None: try: - new_authz_uri = response.links['next']['url'] + new_authzr_uri = response.links['next']['url'] except KeyError: raise errors.NetworkError('"next" link missing') return messages2.RegistrationResource( body=messages2.Registration.from_json(response.json()), - uri=response.headers.get('location', uri), - new_authz_uri=new_authz_uri, + uri=response.headers.get('Location', uri), + new_authzr_uri=new_authzr_uri, terms_of_service=terms_of_service) def register(self, contact=messages2.Registration._fields[ 'contact'].default): """Register. + :param contact: Contact list, as accpeted by `.RegistrationResource` + :type contact: `tuple` + :returns: Registration Resource. :rtype: `.RegistrationResource` @@ -188,11 +192,11 @@ class Network(object): # (c.f. acme-spec #94) updated_regr = self._regr_from_response( - response, uri=regr.uri, new_authz_uri=regr.new_authz_uri) + response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, + terms_of_service=regr.terms_of_service) if updated_regr != regr: - pass # TODO: Boulder reregisters with new recoveryToken and new URI - #raise errors.UnexpectedUpdate(regr) + raise errors.UnexpectedUpdate(regr) return updated_regr def _authzr_from_response(self, response, identifier, @@ -205,7 +209,7 @@ class Network(object): authzr = messages2.AuthorizationResource( body=messages2.Authorization.from_json(response.json()), - uri=response.headers.get('location', uri), + uri=response.headers.get('Location', uri), new_cert_uri=new_cert_uri) if (authzr.body.key != self.key.public() or authzr.body.identifier != identifier): @@ -223,33 +227,44 @@ class Network(object): """ new_authz = messages2.Authorization(identifier=identifier) - response = self._post(regr.new_authz_uri, self._wrap_in_jws(new_authz)) + response = self._post(regr.new_authzr_uri, self._wrap_in_jws(new_authz)) assert response.status_code == httplib.CREATED # TODO: handle errors return self._authzr_from_response(response, identifier) - def answer_challenge(self, challr, response): + def request_domain_challenges(self, domain, regr): + """Request challenges for domain names.""" + return self.request_challenges(messages2.Identifier( + typ=messages2.IDENTIFIER_FQDN, value=domain), regr) + + def answer_challenge(self, challb, response): """Answer challenge. - :param challr: Corresponding challenge resource. - :type challr: `.ChallengeResource` + :param challb: Challenge Resource body. + :type challb: `.ChallengeBody` - :param response: Challenge response + :param response: Corresponding Challenge response :type response: `.challenges.ChallengeResponse` - :returns: Updated challenge resource. + :returns: Challenge resource with updated body. :rtype: `.ChallengeResource` :raises errors.UnexpectedUpdate: """ - response = self._post(challr.uri, self._wrap_in_jws(response)) - if response.headers['location'] != challr.uri: - raise errors.UnexpectedUpdate(response.headers['location']) - updated_challr = challr.update( - body=challenges.Challenge.from_json(response.json())) - return updated_challr + response = self._post(challb.uri, self._wrap_in_jws(response)) + try: + authzr_uri = response.links['up']['url'] + except KeyError: + raise errors.NetworkError('"up" Link header missing') + challr = messages2.ChallengeResource( + authzr_uri=authzr_uri, + body=messages2.ChallengeBody.from_json(response.json())) + # TODO: check that challr.uri == response.headers['Location']? + if challr.uri != challb.uri: + raise errors.UnexpectedUpdate(challr.uri) + return challr - def answer_challenges(self, challrs, responses): + def answer_challenges(self, challbs, responses): """Answer multiple challenges. .. note:: This is a convenience function to make integration @@ -257,18 +272,35 @@ class Network(object): once restification is over. """ - return [self.answer_challenge(challr, response) - for challr, response in itertools.izip(challrs, responses)] + return [self.answer_challenge(challb, response) + for challb, response in itertools.izip(challbs, responses)] @classmethod - def _retry_after(cls, response, mintime): - retry_after = response.headers.get('Retry-After', str(mintime)) + def retry_after(cls, response, default): + """Compute next `poll` time based on response ``Retry-After`` header. + + :param response: Response from `poll`. + :type response: `requests.Response` + + :param int default: Default value (in seconds), used when + ``Retry-After`` header is not present or invalid. + + :returns: Time point when next `poll` should be performed. + :rtype: `datetime.datetime` + + """ + retry_after = response.headers.get('Retry-After', str(default)) try: seconds = int(retry_after) except ValueError: - return werkzeug.parse_date(retry_after) # pylint: disable=no-member - else: - return datetime.datetime.now() + datetime.timedelta(seconds=seconds) + # pylint: disable=no-member + decoded = werkzeug.parse_date(retry_after) # RFC1123 + if decoded is None: + seconds = default + else: + return decoded + + return datetime.datetime.now() + datetime.timedelta(seconds=seconds) def poll(self, authzr): """Poll Authorization Resource for status. @@ -284,7 +316,7 @@ class Network(object): response = self._get(authzr.uri) updated_authzr = self._authzr_from_response( response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri) - # TODO check UnexpectedUpdate + # TODO: check and raise UnexpectedUpdate return updated_authzr, response @@ -292,11 +324,16 @@ class Network(object): """Request issuance. :param csr: CSR - :type csr: `M2Crypto.X509.Request` + :type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` :param authzrs: `list` of `.AuthorizationResource` + :returns: Issued certificate + :rtype: `.messages2.CertificateResource` + """ + assert authzrs, "Authorizations list is empty" + # TODO: assert len(authzrs) == number of SANs req = messages2.CertificateRequest( csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) @@ -308,18 +345,46 @@ class Network(object): content_type=content_type, headers={'Accept': content_type}) + try: + cert_chain_uri = response.links['up']['url'] + except KeyError: + raise errors.NetworkError('"up" Link missing') + + try: + uri = response.headers['Location'] + except KeyError: + raise errors.NetworkError('"Location" Header missing') + return messages2.CertificateResource( - authzrs=authzrs, - body=M2Crypto.X509.load_cert_der_string(response.text), - cert_chain_uri=response.links['up']['url']) + uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, + body=jose.ComparableX509( + M2Crypto.X509.load_cert_der_string(response.content))) def poll_and_request_issuance(self, csr, authzrs, mintime=5): """Poll and request issuance. - :param int mintime: Minimum time before next attempt. + This function polls all provided Authorization Resource URIs + until all challenges are valid, respecting ``Retry-After`` HTTP + headers, and then calls `request_issuance`. .. todo:: add `max_attempts` or `timeout` + :param csr: CSR. + :type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` + + :param authzrs: `list` of `.AuthorizationResource` + + :param int mintime: Minimum time before next attempt, used if + ``Retry-After`` is not present in the response. + + :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is + the issued certificate (`.messages2.CertificateResource.), + and ``updated_authzrs`` is a `tuple` consisting of updated + Authorization Resources (`.AuthorizationResource`) as + present in the responses from server, and in the same order + as the input ``authzrs``. + :rtype: `tuple` + """ # priority queue with datetime (based od Retry-After) as key, # and original Authorization Resource as value @@ -337,25 +402,25 @@ class Network(object): logging.debug('Sleeping for %d seconds', seconds) time.sleep(seconds) - updated_authzr, response = self.poll(authzr) + # Note that we poll with the latest updated Authorization + # URI, which might have a different URI than initial one + updated_authzr, response = self.poll(updated[authzr]) updated[authzr] = updated_authzr - # URI must not change throughout, as we are polling - # original Authorization Resource URI only - assert updated_authzr.uri == authzr if updated_authzr.body.status != messages2.STATUS_VALID: # push back to the priority queue, with updated retry_after - heapq.heappush(waiting, (self._retry_after( - response, mintime=mintime), authzr)) + heapq.heappush(waiting, (self.retry_after( + response, default=mintime), authzr)) - return self.request_issuance(csr, authzrs), tuple( - updated[authzr] for authzr in authzrs) + updated_authzrs = tuple(updated[authzr] for authzr in authzrs) + return self.request_issuance(csr, updated_authzrs), updated_authzrs def _get_cert(self, uri): content_type = self.DER_CONTENT_TYPE # TODO: make it a param response = self._get(uri, headers={'Accept': content_type}, content_type=content_type) - return response, M2Crypto.X509.load_cert_der_string(response.text) + return response, jose.ComparableX509( + M2Crypto.X509.load_cert_der_string(response.content)) def check_cert(self, certr): """Check for new cert. @@ -370,7 +435,9 @@ class Network(object): # TODO: acme-spec 5.1 table action should be renamed to # "refresh cert", and this method integrated with self.refresh response, cert = self._get_cert(certr.uri) - if not response.headers['location'] != certr.uri: + if 'Location' not in response.headers: + raise errors.NetworkError('Location header missing') + if response.headers['Location'] != certr.uri: raise errors.UnexpectedUpdate(response.text) return certr.update(body=cert) @@ -393,7 +460,7 @@ class Network(object): :type certr: `.CertificateResource` :returns: Certificate chain - :rtype: `M2Crypto.X509.X509` + :rtype: `M2Crypto.X509.X509` wrapped in `.ComparableX509` """ return self._get_cert(certr.cert_chain_uri) @@ -401,8 +468,8 @@ class Network(object): def revoke(self, certr, when=messages2.Revocation.NOW): """Revoke certificate. - :param when: When should the revocation take place. - :type when: `.Revocation.When` + :param when: When should the revocation take place? Takes + the same values as `.messages2.Revocation.revoke`. """ rev = messages2.Revocation(revoke=when, authorizations=tuple( diff --git a/letsencrypt/client/tests/network2_test.py b/letsencrypt/client/tests/network2_test.py new file mode 100644 index 000000000..d7aa74929 --- /dev/null +++ b/letsencrypt/client/tests/network2_test.py @@ -0,0 +1,453 @@ +"""Tests for letsencrypt.client.network2.""" +import datetime +import httplib +import os +import pkg_resources +import unittest + +import M2Crypto +import mock +import requests + +from letsencrypt.client import errors + +from letsencrypt.acme import challenges +from letsencrypt.acme import jose +from letsencrypt.acme import messages2 + + +CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string( + pkg_resources.resource_string( + __name__, os.path.join('testdata/cert.pem')))) +CERT2 = jose.ComparableX509(M2Crypto.X509.load_cert_string( + pkg_resources.resource_string( + __name__, os.path.join('testdata/cert-san.pem')))) +CSR = jose.ComparableX509(M2Crypto.X509.load_request_string( + pkg_resources.resource_string( + __name__, os.path.join('testdata/csr.pem')))) +KEY = jose.JWKRSA.load(pkg_resources.resource_string( + __name__, os.path.join('testdata/rsa512_key.pem'))) +KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( + __name__, os.path.join('testdata/rsa256_key.pem'))) + + +class NetworkTest(unittest.TestCase): + """Tests for letsencrypt.client.network2.Network.""" + + # pylint: disable=too-many-instance-attributes,too-many-public-methods + + def setUp(self): + from letsencrypt.client.network2 import Network + self.net = Network( + new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg', + key=KEY, alg=jose.RS256) + self.response = mock.MagicMock(ok=True, status_code=httplib.OK) + self.response.headers = {} + self.response.links = {} + + self.identifier = messages2.Identifier( + typ=messages2.IDENTIFIER_FQDN, value='example.com') + + # Registration + self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') + reg = messages2.Registration( + contact=self.contact, key=KEY.public(), recovery_token='t') + self.regr = messages2.RegistrationResource( + body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1', + new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg', + terms_of_service='https://www.letsencrypt-demo.org/tos') + + # Authorization + authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' + challb = messages2.ChallengeBody( + uri=(authzr_uri + '/1'), status=messages2.STATUS_VALID, + chall=challenges.DNS(token='foo')) + self.challr = messages2.ChallengeResource( + body=challb, authzr_uri=authzr_uri) + self.authz = messages2.Authorization( + identifier=messages2.Identifier( + typ=messages2.IDENTIFIER_FQDN, value='example.com'), + challenges=(challb,), combinations=None, key=KEY.public()) + self.authzr = messages2.AuthorizationResource( + body=self.authz, uri=authzr_uri, + new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert') + + # Request issuance + self.certr = messages2.CertificateResource( + body=CERT, authzrs=(self.authzr,), + uri='https://www.letsencrypt-demo.org/acme/cert/1', + cert_chain_uri='https://www.letsencrypt-demo.org/ca') + + def _mock_post_get(self): + # pylint: disable=protected-access + self.net._post = mock.MagicMock(return_value=self.response) + self.net._get = mock.MagicMock(return_value=self.response) + + def test_wrap_in_jws(self): + class MockJSONDeSerializable(jose.JSONDeSerializable): + # pylint: disable=missing-docstring + def __init__(self, value): + self.value = value + def to_json(self): + return self.value + @classmethod + def from_json(cls, value): + return cls(value) + # pylint: disable=protected-access + jws = self.net._wrap_in_jws(MockJSONDeSerializable('foo')) + self.assertEqual(jose.JWS.json_loads(jws).payload, '"foo"') + + def test_check_response_not_ok_jobj_no_error(self): + self.response.ok = False + self.response.json.return_value = {} + # pylint: disable=protected-access + self.assertRaises( + errors.NetworkError, self.net._check_response, self.response) + + def test_check_response_not_ok_jobj_error(self): + self.response.ok = False + self.response.json.return_value = messages2.Error(detail='foo') + # pylint: disable=protected-access + self.assertRaises( + messages2.Error, self.net._check_response, self.response) + + def test_check_response_not_ok_no_jobj(self): + self.response.ok = False + self.response.json.side_effect = ValueError + # pylint: disable=protected-access + self.assertRaises( + errors.NetworkError, self.net._check_response, self.response) + + def test_check_response_ok_no_jobj_ct_required(self): + self.response.json.side_effect = ValueError + for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: + self.response.headers['Content-Type'] = response_ct + # pylint: disable=protected-access + self.assertRaises( + errors.NetworkError, self.net._check_response, self.response, + content_type=self.net.JSON_CONTENT_TYPE) + + def test_check_response_ok_no_jobj_no_ct(self): + self.response.json.side_effect = ValueError + for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: + self.response.headers['Content-Type'] = response_ct + # pylint: disable=protected-access + self.net._check_response(self.response) + + def test_check_response_jobj(self): + self.response.json.return_value = {} + for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: + self.response.headers['Content-Type'] = response_ct + # pylint: disable=protected-access + self.net._check_response(self.response) + + @mock.patch('letsencrypt.client.network2.requests') + def test_get_requests_error_passthrough(self, requests_mock): + requests_mock.exceptions = requests.exceptions + requests_mock.get.side_effect = requests.exceptions.RequestException + # pylint: disable=protected-access + self.assertRaises(errors.NetworkError, self.net._get, 'uri') + + @mock.patch('letsencrypt.client.network2.requests') + def test_get(self, requests_mock): + # pylint: disable=protected-access + self.net._check_response = mock.MagicMock() + self.net._get('uri', content_type='ct') + self.net._check_response.assert_called_once_with( + requests_mock.get('uri'), content_type='ct') + + @mock.patch('letsencrypt.client.network2.requests') + def test_post_requests_error_passthrough(self, requests_mock): + requests_mock.exceptions = requests.exceptions + requests_mock.post.side_effect = requests.exceptions.RequestException + # pylint: disable=protected-access + self.assertRaises(errors.NetworkError, self.net._post, 'uri', 'data') + + @mock.patch('letsencrypt.client.network2.requests') + def test_post(self, requests_mock): + # pylint: disable=protected-access + self.net._check_response = mock.MagicMock() + self.net._post('uri', 'data', content_type='ct') + self.net._check_response.assert_called_once_with( + requests_mock.post('uri', 'data'), content_type='ct') + + def test_register(self): + self.response.status_code = httplib.CREATED + self.response.json.return_value = self.regr.body.fully_serialize() + self.response.headers['Location'] = self.regr.uri + self.response.links.update({ + 'next': {'url': self.regr.new_authzr_uri}, + 'terms-of-service': {'url': self.regr.terms_of_service}, + }) + + self._mock_post_get() + self.assertEqual(self.regr, self.net.register(self.contact)) + # TODO: test POST call arguments + + # TODO: split here and separate test + reg_wrong_key = self.regr.body.update(key=KEY2.public()) + self.response.json.return_value = reg_wrong_key.fully_serialize() + self.assertRaises( + errors.UnexpectedUpdate, self.net.register, self.contact) + + def test_register_missing_next(self): + self.response.status_code = httplib.CREATED + self._mock_post_get() + self.assertRaises( + errors.NetworkError, self.net.register, self.regr.body) + + def test_update_registration(self): + self.response.headers['Location'] = self.regr.uri + self.response.json.return_value = self.regr.body.fully_serialize() + self._mock_post_get() + self.assertEqual(self.regr, self.net.update_registration(self.regr)) + + # TODO: split here and separate test + self.response.json.return_value = self.regr.body.update( + contact=()).fully_serialize() + self.assertRaises( + errors.UnexpectedUpdate, self.net.update_registration, self.regr) + + def test_request_challenges(self): + self.response.status_code = httplib.CREATED + self.response.headers['Location'] = self.authzr.uri + self.response.json.return_value = self.authz.fully_serialize() + self.response.links = { + 'next': {'url': self.authzr.new_cert_uri}, + } + + self._mock_post_get() + self.net.request_challenges(self.identifier, self.regr) + # TODO: test POST call arguments + + # TODO: split here and separate test + authz_wrong_key = self.authz.update(key=KEY2.public()) + self.response.json.return_value = authz_wrong_key.fully_serialize() + self.assertRaises( + errors.UnexpectedUpdate, self.net.request_challenges, + self.identifier, self.regr) + + def test_request_challenges_missing_next(self): + self.response.status_code = httplib.CREATED + self._mock_post_get() + self.assertRaises( + errors.NetworkError, self.net.request_challenges, + self.identifier, self.regr) + + def test_request_domain_challenges(self): + self.net.request_challenges = mock.MagicMock() + self.assertEqual( + self.net.request_challenges(self.identifier), + self.net.request_domain_challenges('example.com', self.regr)) + + def test_answer_challenge(self): + self.response.links['up'] = {'url': self.challr.authzr_uri} + self.response.json.return_value = self.challr.body.fully_serialize() + + chall_response = challenges.DNSResponse() + + self._mock_post_get() + self.net.answer_challenge(self.challr.body, chall_response) + + # TODO: split here and separate test + self.assertRaises(errors.UnexpectedUpdate, self.net.answer_challenge, + self.challr.body.update(uri='foo'), chall_response) + + def test_answer_challenge_missing_next(self): + self._mock_post_get() + self.assertRaises(errors.NetworkError, self.net.answer_challenge, + self.challr.body, challenges.DNSResponse()) + + def test_answer_challenges(self): + self.net.answer_challenge = mock.MagicMock() + self.assertEqual( + [self.net.answer_challenge( + self.challr.body, challenges.DNSResponse())], + self.net.answer_challenges( + [self.challr.body], [challenges.DNSResponse()])) + + def test_retry_after_date(self): + self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' + self.assertEqual( + datetime.datetime(1999, 12, 31, 23, 59, 59), + self.net.retry_after(response=self.response, default=10)) + + @mock.patch('letsencrypt.client.network2.datetime') + def test_retry_after_invalid(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.response.headers['Retry-After'] = 'foooo' + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.net.retry_after(response=self.response, default=10)) + + @mock.patch('letsencrypt.client.network2.datetime') + def test_retry_after_seconds(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.response.headers['Retry-After'] = '50' + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 50), + self.net.retry_after(response=self.response, default=10)) + + @mock.patch('letsencrypt.client.network2.datetime') + def test_retry_after_missing(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.net.retry_after(response=self.response, default=10)) + + def test_poll(self): + self.response.json.return_value = self.authzr.body.fully_serialize() + self._mock_post_get() + self.assertEqual((self.authzr, self.response), + self.net.poll(self.authzr)) + + def test_request_issuance(self): + self.response.content = CERT.as_der() + self.response.headers['Location'] = self.certr.uri + self.response.links['up'] = {'url': self.certr.cert_chain_uri} + self._mock_post_get() + self.assertEqual( + self.certr, self.net.request_issuance(CSR, (self.authzr,))) + # TODO: check POST args + + def test_request_issuance_missing_up(self): + self._mock_post_get() + self.assertRaises( + errors.NetworkError, self.net.request_issuance, + CSR, (self.authzr,)) + + def test_request_issuance_missing_location(self): + self.response.links['up'] = {'url': self.certr.cert_chain_uri} + self._mock_post_get() + self.assertRaises( + errors.NetworkError, self.net.request_issuance, + CSR, (self.authzr,)) + + @mock.patch('letsencrypt.client.network2.datetime') + @mock.patch('letsencrypt.client.network2.time') + def test_poll_and_request_issuance(self, time_mock, dt_mock): + # clock.dt | pylint: disable=no-member + clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) + + def sleep(seconds): + """increment clock""" + clock.dt += datetime.timedelta(seconds=seconds) + time_mock.sleep.side_effect = sleep + + def now(): + """return current clock value""" + return clock.dt + dt_mock.datetime.now.side_effect = now + dt_mock.timedelta = datetime.timedelta + + def poll(authzr): # pylint: disable=missing-docstring + # record poll start time based on the current clock value + authzr.times.append(clock.dt) + + # suppose it takes 2 seconds for server to produce the + # result, increment clock + clock.dt += datetime.timedelta(seconds=2) + + if not authzr.retries: # no more retries + done = mock.MagicMock(uri=authzr.uri, times=authzr.times) + done.body.status = messages2.STATUS_VALID + return done, [] + + # response (2nd result tuple element) is reduced to only + # Retry-After header contents represented as integer + # seconds; authzr.retries is a list of Retry-After + # headers, head(retries) is peeled of as a current + # Retry-After header, and tail(retries) is persisted for + # later poll() calls + return (mock.MagicMock(retries=authzr.retries[1:], + uri=authzr.uri + '.', times=authzr.times), + authzr.retries[0]) + self.net.poll = mock.MagicMock(side_effect=poll) + + mintime = 7 + + def retry_after(response, default): # pylint: disable=missing-docstring + # check that poll_and_request_issuance correctly passes mintime + self.assertEqual(default, mintime) + return clock.dt + datetime.timedelta(seconds=response) + self.net.retry_after = mock.MagicMock(side_effect=retry_after) + + def request_issuance(csr, authzrs): # pylint: disable=missing-docstring + return csr, authzrs + self.net.request_issuance = mock.MagicMock(side_effect=request_issuance) + + csr = mock.MagicMock() + authzrs = ( + mock.MagicMock(uri='a', times=[], retries=(8, 20, 30)), + mock.MagicMock(uri='b', times=[], retries=(5,)), + ) + + cert, updated_authzrs = self.net.poll_and_request_issuance( + csr, authzrs, mintime=mintime) + self.assertTrue(cert[0] is csr) + self.assertTrue(cert[1] is updated_authzrs) + self.assertEqual(updated_authzrs[0].uri, 'a...') + self.assertEqual(updated_authzrs[1].uri, 'b.') + self.assertEqual(updated_authzrs[0].times, [ + datetime.datetime(2015, 3, 27), + # a is scheduled for 10, but b is polling [9..11), so it + # will be picked up as soon as b is finished, without + # additional sleeping + datetime.datetime(2015, 3, 27, 0, 0, 11), + datetime.datetime(2015, 3, 27, 0, 0, 33), + datetime.datetime(2015, 3, 27, 0, 1, 5), + ]) + self.assertEqual(updated_authzrs[1].times, [ + datetime.datetime(2015, 3, 27, 0, 0, 2), + datetime.datetime(2015, 3, 27, 0, 0, 9), + ]) + self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7)) + + def test_check_cert(self): + self.response.headers['Location'] = self.certr.uri + self.response.content = CERT2.as_der() + self._mock_post_get() + self.assertEqual( + self.certr.update(body=CERT2), self.net.check_cert(self.certr)) + + # TODO: split here and separate test + self.response.headers['Location'] = 'foo' + self.assertRaises( + errors.UnexpectedUpdate, self.net.check_cert, self.certr) + + def test_check_cert_missing_location(self): + self.response.content = CERT2.as_der() + self._mock_post_get() + self.assertRaises(errors.NetworkError, self.net.check_cert, self.certr) + + def test_refresh(self): + self.net.check_cert = mock.MagicMock() + self.assertEqual( + self.net.check_cert(self.certr), self.net.refresh(self.certr)) + + def test_fetch_chain(self): + # pylint: disable=protected-access + self.net._get_cert = mock.MagicMock() + self.assertEqual(self.net._get_cert(self.certr.cert_chain_uri), + self.net.fetch_chain(self.certr)) + + def test_revoke(self): + self._mock_post_get() + self.net.revoke(self.certr, when=messages2.Revocation.NOW) + # pylint: disable=protected-access + self.net._post.assert_called_once_with(self.certr.uri, mock.ANY) + + def test_revoke_bad_status_raises_error(self): + self.response.status_code = httplib.METHOD_NOT_ALLOWED + self._mock_post_get() + self.assertRaises(errors.NetworkError, self.net.revoke, self.certr) + + +if __name__ == '__main__': + unittest.main() From 4b829603d0bc6a8edd76825363fbe585efa1497a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 27 Mar 2015 23:49:09 +0000 Subject: [PATCH 74/92] py26 compat --- letsencrypt/acme/messages2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index f3ce53665..ecb0d9868 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -63,7 +63,7 @@ class _Constant(jose.JSONDeSerializable): def from_json(cls, value): if value not in cls.POSSIBLE_NAMES: raise jose.DeserializationError( - '{} not recognized'.format(cls.__name__)) + '{0} not recognized'.format(cls.__name__)) return cls.POSSIBLE_NAMES[value] def __repr__(self): From 567cec1824b2a319f6f50d32634e206706d2b95e Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 27 Mar 2015 21:08:14 -0700 Subject: [PATCH 75/92] Fix gen_chall_path, add unittests --- letsencrypt/client/auth_handler.py | 35 +++++---- letsencrypt/client/tests/acme_util.py | 22 +++--- letsencrypt/client/tests/auth_handler_test.py | 72 +++++++++++++++++++ tox.ini | 2 +- 4 files changed, 104 insertions(+), 27 deletions(-) diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index 05f3722cf..136265aa6 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -315,24 +315,23 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes def gen_challenge_path(challs, preferences, combinations): """Generate a plan to get authority over the identity. - .. todo:: Make sure that the challenges are feasible... - Example: Do you have the recovery key? + .. todo:: This can be possibly be rewritten to use resolved_combinations. - :param list challs: A list of challenges + :param tuple challs: A tuple of challenges (:class:`letsencrypt.acme.challenges.Challenge`) from :class:`letsencrypt.acme.messages.Challenge` server message to be fulfilled by the client in order to prove possession of the identifier. :param list preferences: List of challenge preferences for domain - (:class:`letsencrypt.acme.challenges.Challege` subclasses) + (:class:`letsencrypt.acme.challenges.Challenge` subclasses) - :param list combinations: A collection of sets of challenges from + :param tuple combinations: A collection of sets of challenges from :class:`letsencrypt.acme.messages.Challenge`, each of which would be sufficient to prove possession of the identifier. - :returns: List of indices from ``challenges``. - :rtype: list + :returns: tuple of indices from ``challenges``. + :rtype: tuple """ if combinations: @@ -349,29 +348,34 @@ def _find_smart_path(challs, preferences, combinations): """ chall_cost = {} - max_cost = 0 + max_cost = 1 for i, chall_cls in enumerate(preferences): chall_cost[chall_cls] = i max_cost += i + # max_cost is now equal to sum(indices) + 1 + best_combo = [] # Set above completing all of the available challenges - best_combo_cost = max_cost + 1 + best_combo_cost = max_cost combo_total = 0 for combo in combinations: for challenge_index in combo: combo_total += chall_cost.get(challs[ challenge_index].__class__, max_cost) + if combo_total < best_combo_cost: best_combo = combo best_combo_cost = combo_total - combo_total = 0 + + combo_total = 0 if not best_combo: - logging.fatal("Client does not support any combination of " - "challenges to satisfy ACME server") - sys.exit(22) + msg = ("Client does not support any combination of challenges that " + "will satisfy the CA.") + logging.fatal(msg) + raise errors.LetsEncryptAuthHandlerError(msg) return best_combo @@ -387,13 +391,14 @@ def _find_dumb_path(challs, preferences): assert len(preferences) == len(set(preferences)) path = [] - satisfied = set() + # This cannot be a set() because POP challenge is not currently hashable + satisfied = [] for pref_c in preferences: for i, offered_chall in enumerate(challs): if (isinstance(offered_chall, pref_c) and is_preferred(offered_chall, satisfied)): path.append(i) - satisfied.add(offered_chall) + satisfied.append(offered_chall) return path diff --git a/letsencrypt/client/tests/acme_util.py b/letsencrypt/client/tests/acme_util.py index aba839f8c..1b121e49f 100644 --- a/letsencrypt/client/tests/acme_util.py +++ b/letsencrypt/client/tests/acme_util.py @@ -27,19 +27,19 @@ POP = challenges.ProofOfPossession( alg="RS256", nonce="xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ", hints=challenges.ProofOfPossession.Hints( jwk=jose.JWKRSA(key=KEY.publickey()), - cert_fingerprints=[ + cert_fingerprints=( "93416768eb85e33adc4277f4c9acd63e7418fcfe", "16d95b7b63f1972b980b14c20291f3c0d1855d95", "48b46570d9fc6358108af43ad1649484def0debf" - ], - certs=[], # TODO - subject_key_identifiers=["d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"], - serial_numbers=[34234239832, 23993939911, 17], - issuers=[ + ), + certs=(), # TODO + subject_key_identifiers=("d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"), + serial_numbers=(34234239832, 23993939911, 17), + issuers=( "C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA", "O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure", - ], - authorized_for=["www.example.com", "example.net"], + ), + authorized_for=("www.example.com", "example.net"), ) ) @@ -61,6 +61,6 @@ def gen_combos(challs): else: renewal_chall.append(i) - # Gen combos for 1 of each type - return [[i, j] for i in xrange(len(dv_chall)) - for j in xrange(len(renewal_chall))] + # Gen combos for 1 of each type, lowest index first (makes testing easier) + return tuple((i, j) if i < j else (j, i) + for i in dv_chall for j in renewal_chall) diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index 478d4c0ac..6150899de 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -513,6 +513,78 @@ class PathSatisfiedTest(unittest.TestCase): self.assertFalse(self.handler._path_satisfied(dom[i])) +class GenChallengePathTest(unittest.TestCase): + """Tests for letsencrypt.client.auth_handler.gen_challenge_path. + + .. todo:: Add more tests for dumb_path... depending on what we want to do. + + """ + def setUp(self): + logging.disable(logging.fatal) + + def tearDown(self): + logging.disable(logging.NOTSET) + + @classmethod + def _call(cls, challs, preferences, combinations): + from letsencrypt.client.auth_handler import gen_challenge_path + return gen_challenge_path(challs, preferences, combinations) + + def test_common_case(self): + """Given DVSNI and SimpleHTTPS with appropriate combos.""" + challs = (acme_util.DVSNI, acme_util.SIMPLE_HTTPS) + 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)) + # Rearrange order... + self.assertEqual(self._call(challs[::-1], prefs, combos), (1,)) + self.assertTrue(self._call(challs[::-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) + prefs = [challenges.RecoveryToken, challenges.DVSNI] + combos = acme_util.gen_combos(challs) + self.assertEqual(self._call(challs, prefs, combos), (0, 2)) + + # dumb_path() trivial test + self.assertTrue(self._call(challs, prefs, None)) + + def test_full_client_server(self): + challs = (acme_util.RECOVERY_TOKEN, + acme_util.RECOVERY_CONTACT, + acme_util.POP, + acme_util.DVSNI, + acme_util.SIMPLE_HTTPS, + acme_util.DNS) + # Typical webserver client that can do everything except DNS + # Attempted to make the order realistic + prefs = [challenges.RecoveryToken, + challenges.ProofOfPossession, + challenges.SimpleHTTPS, + challenges.DVSNI, + challenges.RecoveryContact] + combos = acme_util.gen_combos(challs) + self.assertEqual(self._call(challs, prefs, combos), (0, 4)) + + # Dumb path trivial test + self.assertTrue(self._call(challs, prefs, None)) + + def test_not_supported(self): + challs = (acme_util.POP, acme_util.DVSNI) + prefs = [challenges.DVSNI] + combos = ((0, 1),) + + self.assertRaises(errors.LetsEncryptAuthHandlerError, + self._call, + challs, prefs, combos) + + class MutuallyExclusiveTest(unittest.TestCase): """Tests for letsencrypt.client.auth_handler.mutually_exclusive.""" diff --git a/tox.ini b/tox.ini index bb5ac1bb7..fe9da1865 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ setenv = basepython = python2.7 commands = pip install -e .[testing] - python setup.py nosetests --with-coverage --cover-min-percentage=86 + python setup.py nosetests --with-coverage --cover-min-percentage=87 [testenv:lint] # recent versions of pylint do not support Python 2.6 (#97, #187) From b67068e9865817a15cc958647794beb052d0a86b Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 27 Mar 2015 21:09:03 -0700 Subject: [PATCH 76/92] fix typo in challenges doc --- letsencrypt/acme/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/acme/challenges.py b/letsencrypt/acme/challenges.py index 9227fa1a1..7e107962d 100644 --- a/letsencrypt/acme/challenges.py +++ b/letsencrypt/acme/challenges.py @@ -184,7 +184,7 @@ class ProofOfPossession(ClientChallenge): """Hints for "proofOfPossession" challenge. :ivar jwk: JSON Web Key (:class:`letsencrypt.acme.jose.JWK`) - :ivar list certs: List of :class:`M2Crypto.X509.X509` cetificates. + :ivar list certs: List of :class:`M2Crypto.X509.X509` certificates. """ jwk = jose.Field("jwk", decoder=jose.JWK.from_json) From da14e149b1c88ac24949552a738904da1775664d Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 27 Mar 2015 21:36:06 -0700 Subject: [PATCH 77/92] add exception documentation --- letsencrypt/client/auth_handler.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index 136265aa6..38e2c1c7d 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -333,6 +333,10 @@ def gen_challenge_path(challs, preferences, combinations): :returns: tuple of indices from ``challenges``. :rtype: tuple + :raises letsencrypt.client.errors.LetsEncryptAuthHandlerError: If a + path cannot be created that satisfies the CA given the preferences and + combinations. + """ if combinations: return _find_smart_path(challs, preferences, combinations) From d4594f02ed9491aed85a05fdbba9badde2ee9907 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 28 Mar 2015 07:14:11 +0000 Subject: [PATCH 78/92] HashableRSAKey --- letsencrypt/acme/challenges_test.py | 6 ++++-- letsencrypt/acme/jose/__init__.py | 1 + letsencrypt/acme/jose/jwk.py | 9 +++++++-- letsencrypt/acme/jose/util.py | 20 ++++++++++++++++++ letsencrypt/acme/jose/util_test.py | 29 +++++++++++++++++++++++++++ letsencrypt/acme/messages_test.py | 5 +++-- letsencrypt/acme/other_test.py | 10 +++++---- letsencrypt/client/auth_handler.py | 5 +++-- letsencrypt/client/client.py | 7 ++++--- letsencrypt/client/tests/acme_util.py | 6 ++++-- 10 files changed, 81 insertions(+), 17 deletions(-) diff --git a/letsencrypt/acme/challenges_test.py b/letsencrypt/acme/challenges_test.py index 081560fe1..f1507c7fd 100644 --- a/letsencrypt/acme/challenges_test.py +++ b/letsencrypt/acme/challenges_test.py @@ -13,8 +13,10 @@ from letsencrypt.acme import other CERT = jose.ComparableX509(M2Crypto.X509.load_cert( pkg_resources.resource_filename( 'letsencrypt.client.tests', 'testdata/cert.pem'))) -KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string( - 'letsencrypt.client.tests', os.path.join('testdata', 'rsa256_key.pem'))) +KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( + pkg_resources.resource_string( + 'letsencrypt.client.tests', + os.path.join('testdata', 'rsa256_key.pem')))) class SimpleHTTPSTest(unittest.TestCase): diff --git a/letsencrypt/acme/jose/__init__.py b/letsencrypt/acme/jose/__init__.py index 4c7398b79..20f9ba7d3 100644 --- a/letsencrypt/acme/jose/__init__.py +++ b/letsencrypt/acme/jose/__init__.py @@ -70,5 +70,6 @@ from letsencrypt.acme.jose.jws import JWS from letsencrypt.acme.jose.util import ( ComparableX509, + HashableRSAKey, ImmutableMap, ) diff --git a/letsencrypt/acme/jose/jwk.py b/letsencrypt/acme/jose/jwk.py index 1a83a5305..1b7e00e56 100644 --- a/letsencrypt/acme/jose/jwk.py +++ b/letsencrypt/acme/jose/jwk.py @@ -83,7 +83,11 @@ class JWKOct(JWK): @JWK.register class JWKRSA(JWK): - """RSA JWK.""" + """RSA JWK. + + :ivar key: `Crypto.PublicKey.RSA` wrapped in `.HashableRSAKey` + + """ typ = 'RSA' __slots__ = ('key',) @@ -114,7 +118,8 @@ class JWKRSA(JWK): :rtype: :class:`JWKRSA` """ - return cls(key=Crypto.PublicKey.RSA.importKey(string)) + return cls(key=util.HashableRSAKey( + Crypto.PublicKey.RSA.importKey(string))) def public(self): return type(self)(key=self.key.publickey()) diff --git a/letsencrypt/acme/jose/util.py b/letsencrypt/acme/jose/util.py index 5f516884f..7bac8b866 100644 --- a/letsencrypt/acme/jose/util.py +++ b/letsencrypt/acme/jose/util.py @@ -41,6 +41,26 @@ class ComparableX509(object): # pylint: disable=too-few-public-methods return self.as_der() == other.as_der() +class HashableRSAKey(object): # pylint: disable=too-few-public-methods + """Wrapper for `Crypto.PublicKey.RSA` objects that supports hashing.""" + + def __init__(self, wrapped): + self._wrapped = wrapped + + def __getattr__(self, name): + return getattr(self._wrapped, name) + + def __eq__(self, other): + return self._wrapped == other + + def __hash__(self): + return hash((type(self), self.exportKey(format='DER'))) + + def publickey(self): + """Get wrapped public key.""" + return type(self)(self._wrapped.publickey()) + + class ImmutableMap(collections.Mapping, collections.Hashable): # pylint: disable=too-few-public-methods """Immutable key to value mapping with attribute access.""" diff --git a/letsencrypt/acme/jose/util_test.py b/letsencrypt/acme/jose/util_test.py index 671b45472..14d40b0fd 100644 --- a/letsencrypt/acme/jose/util_test.py +++ b/letsencrypt/acme/jose/util_test.py @@ -1,7 +1,36 @@ """Tests for letsencrypt.acme.jose.util.""" import functools +import os +import pkg_resources import unittest +import Crypto.PublicKey.RSA + + +class HashableRSAKeyTest(unittest.TestCase): + """Tests for letsencrypt.acme.jose.util.HashableRSAKey.""" + + def setUp(self): + from letsencrypt.acme.jose.util import HashableRSAKey + self.key = HashableRSAKey(Crypto.PublicKey.RSA.importKey( + pkg_resources.resource_string( + __name__, os.path.join('testdata', 'rsa256_key.pem')))) + self.key_same = HashableRSAKey(Crypto.PublicKey.RSA.importKey( + pkg_resources.resource_string( + __name__, os.path.join('testdata', 'rsa256_key.pem')))) + + def test_eq(self): + # if __eq__ is not defined, then two HashableRSAKeys with same + # _wrapped do not equate + self.assertEqual(self.key, self.key_same) + + def test_hash(self): + self.assertTrue(isinstance(hash(self.key), int)) + + def test_publickey(self): + from letsencrypt.acme.jose.util import HashableRSAKey + self.assertTrue(isinstance(self.key.publickey(), HashableRSAKey)) + class ImmutableMapTest(unittest.TestCase): """Tests for letsencrypt.acme.jose.util.ImmutableMap.""" diff --git a/letsencrypt/acme/messages_test.py b/letsencrypt/acme/messages_test.py index bd6f4d702..0d15633a5 100644 --- a/letsencrypt/acme/messages_test.py +++ b/letsencrypt/acme/messages_test.py @@ -11,8 +11,9 @@ from letsencrypt.acme import jose from letsencrypt.acme import other -KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string( - 'letsencrypt.client.tests', 'testdata/rsa256_key.pem')) +KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( + pkg_resources.resource_string( + 'letsencrypt.client.tests', 'testdata/rsa256_key.pem'))) CERT = jose.ComparableX509(M2Crypto.X509.load_cert( pkg_resources.resource_filename( 'letsencrypt.client.tests', 'testdata/cert.pem'))) diff --git a/letsencrypt/acme/other_test.py b/letsencrypt/acme/other_test.py index 61c37f6a3..047abe54d 100644 --- a/letsencrypt/acme/other_test.py +++ b/letsencrypt/acme/other_test.py @@ -7,10 +7,12 @@ import Crypto.PublicKey.RSA from letsencrypt.acme import jose -RSA256_KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string( - 'letsencrypt.client.tests', 'testdata/rsa256_key.pem')) -RSA512_KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string( - 'letsencrypt.client.tests', 'testdata/rsa512_key.pem')) +RSA256_KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( + pkg_resources.resource_string( + 'letsencrypt.client.tests', 'testdata/rsa256_key.pem'))) +RSA512_KEY = jose.HashableRSAKey( + Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string( + 'letsencrypt.client.tests', 'testdata/rsa512_key.pem'))) class SignatureTest(unittest.TestCase): diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index 05f3722cf..565be1a2d 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -5,6 +5,7 @@ import sys import Crypto.PublicKey.RSA from letsencrypt.acme import challenges +from letsencrypt.acme import jose from letsencrypt.acme import messages from letsencrypt.client import achallenges @@ -119,8 +120,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes nonce=self.msgs[domain].nonce, responses=self.responses[domain], name=domain, - key=Crypto.PublicKey.RSA.importKey( - self.authkey[domain].pem)), + key=jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( + self.authkey[domain].pem))), messages.Authorization) logging.info("Received Authorization for %s", domain) return auth diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 2f3f9a769..e66c45dc2 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -6,8 +6,8 @@ import sys import Crypto.PublicKey.RSA import M2Crypto +from letsencrypt.acme import jose from letsencrypt.acme import messages -from letsencrypt.acme.jose import util as jose_util from letsencrypt.client import auth_handler from letsencrypt.client import client_authenticator @@ -130,9 +130,10 @@ class Client(object): logging.info("Preparing and sending CSR...") return self.network.send_and_receive_expected( messages.CertificateRequest.create( - csr=jose_util.ComparableX509( + csr=jose.ComparableX509( M2Crypto.X509.load_request_der_string(csr_der)), - key=Crypto.PublicKey.RSA.importKey(self.authkey.pem)), + key=jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( + self.authkey.pem))), messages.Certificate) def save_certificate(self, certificate_msg, cert_path, chain_path): diff --git a/letsencrypt/client/tests/acme_util.py b/letsencrypt/client/tests/acme_util.py index aba839f8c..be47bccfd 100644 --- a/letsencrypt/client/tests/acme_util.py +++ b/letsencrypt/client/tests/acme_util.py @@ -8,8 +8,10 @@ from letsencrypt.acme import challenges from letsencrypt.acme import jose -KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string( - "letsencrypt.client.tests", os.path.join("testdata", "rsa256_key.pem"))) +KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( + pkg_resources.resource_string( + "letsencrypt.client.tests", + os.path.join("testdata", "rsa256_key.pem")))) # Challenges SIMPLE_HTTPS = challenges.SimpleHTTPS( From cd0b99ae5d14e14de15166dc48dce1839fe6f0f4 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sun, 29 Mar 2015 23:11:05 -0700 Subject: [PATCH 79/92] Fix ambiguity about describing a port as "open" --- letsencrypt/client/plugins/standalone/authenticator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client/plugins/standalone/authenticator.py b/letsencrypt/client/plugins/standalone/authenticator.py index 22597eba7..e0b06aa30 100644 --- a/letsencrypt/client/plugins/standalone/authenticator.py +++ b/letsencrypt/client/plugins/standalone/authenticator.py @@ -410,5 +410,5 @@ class StandaloneAuthenticator(object): "on port 443 and perform DVSNI challenges. Once a certificate" "is attained, it will be saved in the " "(TODO) current working directory.{0}{0}" - "Port 443 must be open in order to use the " + "TCP port 443 must be available in order to use the " "Standalone Authenticator.".format(os.linesep)) From 8561de7e73896da9043a437463ab838b104d7758 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 30 Mar 2015 12:09:07 -0700 Subject: [PATCH 80/92] Small doc change and formatting --- letsencrypt/acme/challenges.py | 3 ++- letsencrypt/client/tests/auth_handler_test.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/acme/challenges.py b/letsencrypt/acme/challenges.py index 7e107962d..0ff4306a5 100644 --- a/letsencrypt/acme/challenges.py +++ b/letsencrypt/acme/challenges.py @@ -184,7 +184,8 @@ class ProofOfPossession(ClientChallenge): """Hints for "proofOfPossession" challenge. :ivar jwk: JSON Web Key (:class:`letsencrypt.acme.jose.JWK`) - :ivar list certs: List of :class:`M2Crypto.X509.X509` certificates. + :ivar list certs: List of :class:`letsencrypt.acme.jose.ComparableX509` + certificates. """ jwk = jose.Field("jwk", decoder=jose.JWK.from_json) diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index 6150899de..106c1230f 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -581,8 +581,7 @@ class GenChallengePathTest(unittest.TestCase): combos = ((0, 1),) self.assertRaises(errors.LetsEncryptAuthHandlerError, - self._call, - challs, prefs, combos) + self._call, challs, prefs, combos) class MutuallyExclusiveTest(unittest.TestCase): From 9ffcbf9934f3dbba90d8e84299c80c43abe8adbd Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 30 Mar 2015 13:33:44 -0700 Subject: [PATCH 81/92] revert to set --- letsencrypt/client/auth_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index c264ee239..f0b257984 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -397,13 +397,13 @@ def _find_dumb_path(challs, preferences): path = [] # This cannot be a set() because POP challenge is not currently hashable - satisfied = [] + satisfied = set() for pref_c in preferences: for i, offered_chall in enumerate(challs): if (isinstance(offered_chall, pref_c) and is_preferred(offered_chall, satisfied)): path.append(i) - satisfied.append(offered_chall) + satisfied.add(offered_chall) return path From 1c254d64ef84d798a39f95c98210bf6246aefdb9 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 30 Mar 2015 13:54:06 -0700 Subject: [PATCH 82/92] remove old comment --- letsencrypt/client/auth_handler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index f0b257984..72843332b 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -396,7 +396,6 @@ def _find_dumb_path(challs, preferences): assert len(preferences) == len(set(preferences)) path = [] - # This cannot be a set() because POP challenge is not currently hashable satisfied = set() for pref_c in preferences: for i, offered_chall in enumerate(challs): From d4336b3ca138f7a59605c739fca20b7790cb3bf0 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 30 Mar 2015 16:01:26 -0700 Subject: [PATCH 83/92] finish renaming/shorten name --- docs/api/client/client_authenticator.rst | 5 ----- docs/api/client/continuity_auth.rst | 5 +++++ letsencrypt/client/client.py | 9 ++++---- ...ty_authenticator.py => continuity_auth.py} | 4 ++-- letsencrypt/client/errors.py | 2 +- letsencrypt/client/tests/auth_handler_test.py | 22 +++++++++---------- ...icator_test.py => continuity_auth_test.py} | 8 +++---- 7 files changed, 28 insertions(+), 27 deletions(-) delete mode 100644 docs/api/client/client_authenticator.rst create mode 100644 docs/api/client/continuity_auth.rst rename letsencrypt/client/{continuity_authenticator.py => continuity_auth.py} (91%) rename letsencrypt/client/tests/{continuity_authenticator_test.py => continuity_auth_test.py} (88%) diff --git a/docs/api/client/client_authenticator.rst b/docs/api/client/client_authenticator.rst deleted file mode 100644 index 267a0dd50..000000000 --- a/docs/api/client/client_authenticator.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.client.client_authenticator` ----------------------------------------------- - -.. automodule:: letsencrypt.client.client_authenticator - :members: diff --git a/docs/api/client/continuity_auth.rst b/docs/api/client/continuity_auth.rst new file mode 100644 index 000000000..d143a7a79 --- /dev/null +++ b/docs/api/client/continuity_auth.rst @@ -0,0 +1,5 @@ +:mod:`letsencrypt.client.continuity_auth` +---------------------------------------------- + +.. automodule:: letsencrypt.client.continuity_auth + :members: diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 25a1cc1f6..61b9a8de3 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -10,7 +10,7 @@ from letsencrypt.acme import messages from letsencrypt.acme import util as acme_util from letsencrypt.client import auth_handler -from letsencrypt.client import continuity_authenticator +from letsencrypt.client import continuity_auth from letsencrypt.client import crypto_util from letsencrypt.client import errors from letsencrypt.client import le_util @@ -33,7 +33,8 @@ class Client(object): :type authkey: :class:`letsencrypt.client.le_util.Key` :ivar auth_handler: Object that supports the IAuthenticator interface. - auth_handler contains both a dv_authenticator and a continuity_authenticator + auth_handler contains both a dv_authenticator and a + continuity_authenticator :type auth_handler: :class:`letsencrypt.client.auth_handler.AuthHandler` :ivar installer: Object supporting the IInstaller interface. @@ -60,9 +61,9 @@ class Client(object): self.config = config if dv_auth is not None: - client_auth = continuity_authenticator.ContinuityAuthenticator(config) + cont_auth = continuity_auth.ContinuityAuthenticator(config) self.auth_handler = auth_handler.AuthHandler( - dv_auth, client_auth, self.network) + dv_auth, cont_auth, self.network) else: self.auth_handler = None diff --git a/letsencrypt/client/continuity_authenticator.py b/letsencrypt/client/continuity_auth.py similarity index 91% rename from letsencrypt/client/continuity_authenticator.py rename to letsencrypt/client/continuity_auth.py index af979a7c2..4db5a177e 100644 --- a/letsencrypt/client/continuity_authenticator.py +++ b/letsencrypt/client/continuity_auth.py @@ -41,7 +41,7 @@ class ContinuityAuthenticator(object): if isinstance(achall, achallenges.RecoveryToken): responses.append(self.rec_token.perform(achall)) else: - raise errors.LetsEncryptClientAuthError("Unexpected Challenge") + raise errors.LetsEncryptContAuthError("Unexpected Challenge") return responses def cleanup(self, achalls): @@ -50,4 +50,4 @@ class ContinuityAuthenticator(object): if isinstance(achall, achallenges.RecoveryToken): self.rec_token.cleanup(achall) else: - raise errors.LetsEncryptClientAuthError("Unexpected Challenge") + raise errors.LetsEncryptContAuthError("Unexpected Challenge") diff --git a/letsencrypt/client/errors.py b/letsencrypt/client/errors.py index c1d6c785f..23bfc8000 100644 --- a/letsencrypt/client/errors.py +++ b/letsencrypt/client/errors.py @@ -14,7 +14,7 @@ class LetsEncryptAuthHandlerError(LetsEncryptClientError): """Let's Encrypt Auth Handler error.""" -class LetsEncryptClientAuthError(LetsEncryptAuthHandlerError): +class LetsEncryptContAuthError(LetsEncryptAuthHandlerError): """Let's Encrypt Client Authenticator error.""" diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index 3349ebdf9..b26b61b3d 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -30,17 +30,17 @@ class SatisfyChallengesTest(unittest.TestCase): from letsencrypt.client.auth_handler import AuthHandler self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator") - self.mock_client_auth = mock.MagicMock(name="ContinuityAuthenticator") + self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator") self.mock_dv_auth.get_chall_pref.return_value = [challenges.DVSNI] - self.mock_client_auth.get_chall_pref.return_value = [ + self.mock_cont_auth.get_chall_pref.return_value = [ challenges.RecoveryToken] - self.mock_client_auth.perform.side_effect = gen_auth_resp + self.mock_cont_auth.perform.side_effect = gen_auth_resp self.mock_dv_auth.perform.side_effect = gen_auth_resp self.handler = AuthHandler( - self.mock_dv_auth, self.mock_client_auth, None) + self.mock_dv_auth, self.mock_cont_auth, None) logging.disable(logging.CRITICAL) @@ -78,7 +78,7 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.responses[dom]), 1) # Test if statement for dv_auth perform - self.assertEqual(self.mock_client_auth.perform.call_count, 1) + 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]) @@ -106,7 +106,7 @@ class SatisfyChallengesTest(unittest.TestCase): # Each message contains 1 auth, 0 client # Test proper call count for methods - self.assertEqual(self.mock_client_auth.perform.call_count, 0) + 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): @@ -141,7 +141,7 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.client_c), 1) # Test if statement for client_auth perform - self.assertEqual(self.mock_client_auth.perform.call_count, 0) + self.assertEqual(self.mock_cont_auth.perform.call_count, 0) self.assertEqual(self.mock_dv_auth.perform.call_count, 1) self.assertEqual( @@ -309,11 +309,11 @@ class SatisfyChallengesTest(unittest.TestCase): # Verify cleanup is actually run correctly self.assertEqual(self.mock_dv_auth.cleanup.call_count, 2) - self.assertEqual(self.mock_client_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 - client_cleanup_args = self.mock_client_auth.cleanup.call_args_list + client_cleanup_args = self.mock_cont_auth.cleanup.call_args_list # Check DV cleanup for i in xrange(2): @@ -346,7 +346,7 @@ class GetAuthorizationsTest(unittest.TestCase): from letsencrypt.client.auth_handler import AuthHandler self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator") - self.mock_client_auth = mock.MagicMock(name="ContinuityAuthenticator") + 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") @@ -354,7 +354,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.iteration = 0 self.handler = AuthHandler( - self.mock_dv_auth, self.mock_client_auth, None) + 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 diff --git a/letsencrypt/client/tests/continuity_authenticator_test.py b/letsencrypt/client/tests/continuity_auth_test.py similarity index 88% rename from letsencrypt/client/tests/continuity_authenticator_test.py rename to letsencrypt/client/tests/continuity_auth_test.py index 1f1d8f3f8..c1f4a229c 100644 --- a/letsencrypt/client/tests/continuity_authenticator_test.py +++ b/letsencrypt/client/tests/continuity_auth_test.py @@ -13,7 +13,7 @@ class PerformTest(unittest.TestCase): """Test client perform function.""" def setUp(self): - from letsencrypt.client.continuity_authenticator import ContinuityAuthenticator + from letsencrypt.client.continuity_auth import ContinuityAuthenticator self.auth = ContinuityAuthenticator( mock.MagicMock(server="demo_server.org")) @@ -38,7 +38,7 @@ class PerformTest(unittest.TestCase): def test_unexpected(self): self.assertRaises( - errors.LetsEncryptClientAuthError, self.auth.perform, [ + errors.LetsEncryptContAuthError, self.auth.perform, [ achallenges.DVSNI(chall=None, domain="0", key="invalid_key")]) def test_chall_pref(self): @@ -50,7 +50,7 @@ class CleanupTest(unittest.TestCase): """Test the Authenticator cleanup function.""" def setUp(self): - from letsencrypt.client.continuity_authenticator import ContinuityAuthenticator + from letsencrypt.client.continuity_auth import ContinuityAuthenticator self.auth = ContinuityAuthenticator( mock.MagicMock(server="demo_server.org")) @@ -70,7 +70,7 @@ class CleanupTest(unittest.TestCase): token = achallenges.RecoveryToken(chall=None, domain="0") unexpected = achallenges.DVSNI(chall=None, domain="0", key="dummy_key") - self.assertRaises(errors.LetsEncryptClientAuthError, + self.assertRaises(errors.LetsEncryptContAuthError, self.auth.cleanup, [token, unexpected]) From 26074c1399503ce19f78a07da5a8ad16d79d343a Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 30 Mar 2015 17:13:27 -0700 Subject: [PATCH 84/92] rid project of refs to client challenges --- docs/contributing.rst | 17 ++++---- letsencrypt/acme/challenges.py | 8 ++-- letsencrypt/client/auth_handler.py | 60 +++++++++++++-------------- letsencrypt/client/continuity_auth.py | 4 +- letsencrypt/client/tests/acme_util.py | 4 +- 5 files changed, 47 insertions(+), 46 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index e3b81b3d4..e899f36a0 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -98,15 +98,16 @@ the ACME server. From the protocol, there are essentially two different types of challenges. Challenges that must be solved by individual plugins in order to satisfy domain validation (subclasses of `~.DVChallenge`, i.e. `~.challenges.DVSNI`, -`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and client specific -challenges (subclasses of `~.ClientChallenge`, +`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and continuity specific +challenges (subclasses of `~.ContinuityChallenge`, i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`, -`~.challenges.ProofOfPossession`). Client specific challenges are -always handled by the `~.ClientAuthenticator`. Right now we have two -DV Authenticators, `~.ApacheConfigurator` and the -`~.StandaloneAuthenticator`. The Standalone and Apache authenticators -only solve the `~.challenges.DVSNI` challenge currently. (You can set -which challenges your authenticator can handle through the +`~.challenges.ProofOfPossession`). Continuity challenges are +always handled by the `~.ContinuityAuthenticator`, while plugins are +expected to handle `~.DVChallenge` types. +Right now, we have two authenticator plugins, the `~.ApacheConfigurator` +and the `~.StandaloneAuthenticator`. The Standalone and Apache +authenticators only solve the `~.challenges.DVSNI` challenge currently. +(You can set which challenges your authenticator can handle through the :meth:`~.IAuthenticator.get_chall_pref`. (FYI: We also have a partial implementation for a `~.DNSAuthenticator` diff --git a/letsencrypt/acme/challenges.py b/letsencrypt/acme/challenges.py index 0ff4306a5..7a51d7447 100644 --- a/letsencrypt/acme/challenges.py +++ b/letsencrypt/acme/challenges.py @@ -18,7 +18,7 @@ class Challenge(jose.TypedJSONObjectWithFields): TYPES = {} -class ClientChallenge(Challenge): # pylint: disable=abstract-method +class ContinuityChallenge(Challenge): # pylint: disable=abstract-method """Client validation challenges.""" @@ -139,7 +139,7 @@ class DVSNIResponse(ChallengeResponse): return self.z(chall) + self.DOMAIN_SUFFIX @Challenge.register -class RecoveryContact(ClientChallenge): +class RecoveryContact(ContinuityChallenge): """ACME "recoveryContact" challenge.""" typ = "recoveryContact" @@ -156,7 +156,7 @@ class RecoveryContactResponse(ChallengeResponse): @Challenge.register -class RecoveryToken(ClientChallenge): +class RecoveryToken(ContinuityChallenge): """ACME "recoveryToken" challenge.""" typ = "recoveryToken" @@ -169,7 +169,7 @@ class RecoveryTokenResponse(ChallengeResponse): @Challenge.register -class ProofOfPossession(ClientChallenge): +class ProofOfPossession(ContinuityChallenge): """ACME "proofOfPossession" challenge. :ivar str nonce: Random data, **not** base64-encoded. diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index 72843332b..571c51927 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -17,12 +17,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes """ACME Authorization Handler for a client. :ivar dv_auth: Authenticator capable of solving - :const:`~letsencrypt.client.constants.DV_CHALLENGES` + :const:`~letsencrypt.acme.challenges.DVChallenge`(s) :type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` - :ivar client_auth: Authenticator capable of solving - :const:`~letsencrypt.client_auth.constants.CLIENT_CHALLENGES` - :type client_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` + :ivar cont_auth: Authenticator capable of solving + :const:`~letsencrypt.acme.challenges.ContinuityChallenge`(s) + :type cont_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` :ivar network: Network object for sending and receiving authorization messages @@ -37,13 +37,13 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes :ivar dict paths: optimal path for authorization. eg. paths[domain] :ivar dict dv_c: Keys - domain, Values are DV challenges in the form of :class:`letsencrypt.client.achallenges.Indexed` - :ivar dict client_c: Keys - domain, Values are Client challenges in the form - of :class:`letsencrypt.client.achallenges.Indexed` + :ivar dict cont_c: Keys - domain, Values are Continuity challenges in the + form of :class:`letsencrypt.client.achallenges.Indexed` """ - def __init__(self, dv_auth, client_auth, network): + def __init__(self, dv_auth, cont_auth, network): self.dv_auth = dv_auth - self.client_auth = client_auth + self.cont_auth = cont_auth self.network = network self.domains = [] @@ -53,7 +53,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes self.paths = dict() self.dv_c = dict() - self.client_c = dict() + self.cont_c = dict() def add_chall_msg(self, domain, msg, authkey): """Add a challenge message to the AuthHandler. @@ -77,7 +77,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes self.authkey[domain] = authkey def get_authorizations(self): - """Retreive all authorizations for challenges. + """Retrieve all authorizations for challenges. :raises LetsEncryptAuthHandlerError: If unable to retrieve all authorizations @@ -148,24 +148,24 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes self._get_chall_pref(dom), self.msgs[dom].combinations) - self.dv_c[dom], self.client_c[dom] = self._challenge_factory( + self.dv_c[dom], self.cont_c[dom] = self._challenge_factory( dom, self.paths[dom]) # Flatten challs for authenticator functions and remove index # Order is important here as we will not expose the outside # Authenticator to our own indices. - flat_client = [] + flat_cont = [] flat_dv = [] for dom in self.domains: - flat_client.extend(ichall.achall for ichall in self.client_c[dom]) + flat_cont.extend(ichall.achall for ichall in self.cont_c[dom]) flat_dv.extend(ichall.achall for ichall in self.dv_c[dom]) - client_resp = [] + cont_resp = [] dv_resp = [] try: - if flat_client: - client_resp = self.client_auth.perform(flat_client) + if flat_cont: + cont_resp = self.cont_auth.perform(flat_cont) if flat_dv: dv_resp = self.dv_auth.perform(flat_dv) # This will catch both specific types of errors. @@ -182,8 +182,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes logging.info("Ready for verification...") # Assemble Responses - if client_resp: - self._assign_responses(client_resp, self.client_c) + if cont_resp: + self._assign_responses(cont_resp, self.cont_c) if dv_resp: self._assign_responses(dv_resp, self.dv_c) @@ -192,7 +192,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes :param list flat_list: flat_list of responses from an IAuthenticator :param dict ichall_dict: Master dict mapping all domains to a list of - their associated 'client' and 'dv' Indexed challenges, or their + their associated 'continuity' and 'dv' Indexed challenges, or their :class:`letsencrypt.client.achallenges.Indexed` list """ @@ -214,7 +214,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes """ chall_prefs = [] - chall_prefs.extend(self.client_auth.get_chall_pref(domain)) + chall_prefs.extend(self.cont_auth.get_chall_pref(domain)) chall_prefs.extend(self.dv_auth.get_chall_pref(domain)) return chall_prefs @@ -229,11 +229,11 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes # Chose to make these lists instead of a generator to make it easier to # work with... dv_list = [ichall.achall for ichall in self.dv_c[domain]] - client_list = [ichall.achall for ichall in self.client_c[domain]] + cont_list = [ichall.achall for ichall in self.cont_c[domain]] if dv_list: self.dv_auth.cleanup(dv_list) - if client_list: - self.client_auth.cleanup(client_list) + if cont_list: + self.cont_auth.cleanup(cont_list) def _cleanup_state(self, delete_list): """Cleanup state after an authorization is received. @@ -248,7 +248,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes del self.authkey[domain] - del self.client_c[domain] + del self.cont_c[domain] del self.dv_c[domain] self.domains.remove(domain) @@ -260,9 +260,9 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes :param list path: List of indices from `challenges`. - :returns: dv_chall, list of + :returns: dv_chall, list of DVChallenge type :class:`letsencrypt.client.achallenges.Indexed` - client_chall, list of + cont_chall, list of ContinuityChallenge type :class:`letsencrypt.client.achallenges.Indexed` :rtype: tuple @@ -271,7 +271,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes """ dv_chall = [] - client_chall = [] + cont_chall = [] for index in path: chall = self.msgs[domain].challenges[index] @@ -305,12 +305,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes ichall = achallenges.Indexed(achall=achall, index=index) - if isinstance(chall, challenges.ClientChallenge): - client_chall.append(ichall) + if isinstance(chall, challenges.ContinuityChallenge): + cont_chall.append(ichall) elif isinstance(chall, challenges.DVChallenge): dv_chall.append(ichall) - return dv_chall, client_chall + return dv_chall, cont_chall def gen_challenge_path(challs, preferences, combinations): diff --git a/letsencrypt/client/continuity_auth.py b/letsencrypt/client/continuity_auth.py index 4db5a177e..7603ad166 100644 --- a/letsencrypt/client/continuity_auth.py +++ b/letsencrypt/client/continuity_auth.py @@ -1,4 +1,4 @@ -"""Client Authenticator""" +"""Continuity Authenticator""" import zope.interface from letsencrypt.acme import challenges @@ -11,7 +11,7 @@ from letsencrypt.client import recovery_token class ContinuityAuthenticator(object): """IAuthenticator for - :const:`~letsencrypt.client.constants.CLIENT_CHALLENGES`. + :const:`~letsencrypt.acme.challenges.ContinuityChallenge`s. :ivar rec_token: Performs "recoveryToken" challenges :type rec_token: :class:`letsencrypt.client.recovery_token.RecoveryToken` diff --git a/letsencrypt/client/tests/acme_util.py b/letsencrypt/client/tests/acme_util.py index 98bf20937..12bb6f775 100644 --- a/letsencrypt/client/tests/acme_util.py +++ b/letsencrypt/client/tests/acme_util.py @@ -48,8 +48,8 @@ POP = challenges.ProofOfPossession( CHALLENGES = [SIMPLE_HTTPS, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP] DV_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.DVChallenge)] -CLIENT_CHALLENGES = [chall for chall in CHALLENGES - if isinstance(chall, challenges.ClientChallenge)] +CONT_CHALLENGES = [chall for chall in CHALLENGES + if isinstance(chall, challenges.ContinuityChallenge)] def gen_combos(challs): From dd3f4acbd0bc2a8cf1b73774f28e1229c2df37df Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 30 Mar 2015 17:15:08 -0700 Subject: [PATCH 85/92] rename renewal->cont --- letsencrypt/client/tests/acme_util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/tests/acme_util.py b/letsencrypt/client/tests/acme_util.py index 12bb6f775..f5f6be6f3 100644 --- a/letsencrypt/client/tests/acme_util.py +++ b/letsencrypt/client/tests/acme_util.py @@ -55,14 +55,14 @@ CONT_CHALLENGES = [chall for chall in CHALLENGES def gen_combos(challs): """Generate natural combinations for challs.""" dv_chall = [] - renewal_chall = [] + cont_chall = [] for i, chall in enumerate(challs): # pylint: disable=redefined-outer-name if isinstance(chall, challenges.DVChallenge): dv_chall.append(i) else: - renewal_chall.append(i) + cont_chall.append(i) # Gen combos for 1 of each type, lowest index first (makes testing easier) return tuple((i, j) if i < j else (j, i) - for i in dv_chall for j in renewal_chall) + for i in dv_chall for j in cont_chall) From f7619c620493b691e558881044e2f74d06b76d47 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 30 Mar 2015 17:28:50 -0700 Subject: [PATCH 86/92] fix unittests/formatting --- letsencrypt/client/tests/acme_util.py | 2 +- letsencrypt/client/tests/auth_handler_test.py | 56 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/letsencrypt/client/tests/acme_util.py b/letsencrypt/client/tests/acme_util.py index f5f6be6f3..5a2e2b16f 100644 --- a/letsencrypt/client/tests/acme_util.py +++ b/letsencrypt/client/tests/acme_util.py @@ -49,7 +49,7 @@ CHALLENGES = [SIMPLE_HTTPS, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP] DV_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.DVChallenge)] CONT_CHALLENGES = [chall for chall in CHALLENGES - if isinstance(chall, challenges.ContinuityChallenge)] + if isinstance(chall, challenges.ContinuityChallenge)] def gen_combos(challs): diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index 0ad68cd0e..b9508709d 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -61,9 +61,9 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual("DVSNI0", self.handler.responses[dom][0]) self.assertEqual(len(self.handler.dv_c), 1) - self.assertEqual(len(self.handler.client_c), 1) + self.assertEqual(len(self.handler.cont_c), 1) self.assertEqual(len(self.handler.dv_c[dom]), 1) - self.assertEqual(len(self.handler.client_c[dom]), 0) + self.assertEqual(len(self.handler.cont_c[dom]), 0) def test_name1_rectok1(self): dom = "0" @@ -84,10 +84,10 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual("RecoveryToken0", self.handler.responses[dom][0]) # Assert 1 domain self.assertEqual(len(self.handler.dv_c), 1) - self.assertEqual(len(self.handler.client_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.client_c[dom]), 1) + self.assertEqual(len(self.handler.cont_c[dom]), 1) def test_name5_dvsni5(self): for i in xrange(5): @@ -102,7 +102,7 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.responses), 5) self.assertEqual(len(self.handler.dv_c), 5) - self.assertEqual(len(self.handler.client_c), 5) + self.assertEqual(len(self.handler.cont_c), 5) # Each message contains 1 auth, 0 client # Test proper call count for methods @@ -114,7 +114,7 @@ class SatisfyChallengesTest(unittest.TestCase): 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.client_c[dom]), 0) + self.assertEqual(len(self.handler.cont_c[dom]), 0) self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall, achallenges.DVSNI)) @@ -138,9 +138,9 @@ class SatisfyChallengesTest(unittest.TestCase): 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.client_c), 1) + self.assertEqual(len(self.handler.cont_c), 1) - # Test if statement for client_auth perform + # 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) @@ -149,7 +149,7 @@ class SatisfyChallengesTest(unittest.TestCase): self._get_exp_response(dom, path, acme_util.DV_CHALLENGES)) self.assertEqual(len(self.handler.dv_c[dom]), 1) - self.assertEqual(len(self.handler.client_c[dom]), 0) + self.assertEqual(len(self.handler.cont_c[dom]), 0) self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall, achallenges.SimpleHTTPS)) @@ -175,16 +175,16 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual( len(self.handler.responses[dom]), len(acme_util.CHALLENGES)) self.assertEqual(len(self.handler.dv_c), 1) - self.assertEqual(len(self.handler.client_c), 1) + self.assertEqual(len(self.handler.cont_c), 1) self.assertEqual(len(self.handler.dv_c[dom]), 1) - self.assertEqual(len(self.handler.client_c[dom]), 1) + self.assertEqual(len(self.handler.cont_c[dom]), 1) 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.client_c[dom][0].achall, + self.assertTrue(isinstance(self.handler.cont_c[dom][0].achall, achallenges.RecoveryToken)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") @@ -209,7 +209,7 @@ class SatisfyChallengesTest(unittest.TestCase): 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.client_c), 5) + self.assertEqual(len(self.handler.cont_c), 5) for i in xrange(5): dom = str(i) @@ -217,11 +217,11 @@ class SatisfyChallengesTest(unittest.TestCase): 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.client_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.client_c[dom][0].achall, + self.assertTrue(isinstance(self.handler.cont_c[dom][0].achall, achallenges.RecoveryContact)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") @@ -255,7 +255,7 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.responses), 5) self.assertEqual(len(self.handler.dv_c), 5) - self.assertEqual(len(self.handler.client_c), 5) + self.assertEqual(len(self.handler.cont_c), 5) for i in xrange(5): dom = str(i) @@ -263,7 +263,7 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(self.handler.responses[dom], resp) self.assertEqual(len(self.handler.dv_c[dom]), 1) self.assertEqual( - len(self.handler.client_c[dom]), len(chosen_chall[i]) - 1) + len(self.handler.cont_c[dom]), len(chosen_chall[i]) - 1) self.assertTrue(isinstance( self.handler.dv_c["0"][0].achall, achallenges.DNS)) @@ -276,10 +276,10 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertTrue(isinstance( self.handler.dv_c["4"][0].achall, achallenges.DNS)) - self.assertTrue(isinstance(self.handler.client_c["2"][0].achall, + self.assertTrue(isinstance(self.handler.cont_c["2"][0].achall, achallenges.ProofOfPossession)) self.assertTrue(isinstance( - self.handler.client_c["4"][0].achall, achallenges.RecoveryToken)) + 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): @@ -313,7 +313,7 @@ class SatisfyChallengesTest(unittest.TestCase): dv_cleanup_args = self.mock_dv_auth.cleanup.call_args_list - client_cleanup_args = self.mock_cont_auth.cleanup.call_args_list + cont_cleanup_args = self.mock_cont_auth.cleanup.call_args_list # Check DV cleanup for i in xrange(2): @@ -325,10 +325,10 @@ class SatisfyChallengesTest(unittest.TestCase): # Check Auth cleanup for i in xrange(2): - client_chall_list = client_cleanup_args[i][0][0] - self.assertEqual(len(client_chall_list), 1) + cont_chall_list = cont_cleanup_args[i][0][0] + self.assertEqual(len(cont_chall_list), 1) self.assertTrue( - isinstance(client_chall_list[0], achallenges.ProofOfPossession)) + isinstance(cont_chall_list[0], achallenges.ProofOfPossession)) def _get_exp_response(self, domain, path, challs): @@ -388,7 +388,7 @@ class GetAuthorizationsTest(unittest.TestCase): # Assignment was > 80 char... dv_c, c_c = self.handler._challenge_factory(dom, [0]) - self.handler.dv_c[dom], self.handler.client_c[dom] = dv_c, c_c + self.handler.dv_c[dom], self.handler.cont_c[dom] = dv_c, c_c def test_progress_failure(self): self.handler.add_chall_msg( @@ -414,7 +414,7 @@ class GetAuthorizationsTest(unittest.TestCase): 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.client_c[dom] = dv_c, c_c + 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), @@ -444,9 +444,9 @@ class GetAuthorizationsTest(unittest.TestCase): 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.client_c["0"] = dv_c, c_c + 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.client_c["1"] = dv_c, c_c + self.handler.dv_c["1"], self.handler.cont_c["1"] = dv_c, c_c self.iteration += 1 @@ -555,7 +555,7 @@ class GenChallengePathTest(unittest.TestCase): # dumb_path() trivial test self.assertTrue(self._call(challs, prefs, None)) - def test_full_client_server(self): + def test_full_cont_server(self): challs = (acme_util.RECOVERY_TOKEN, acme_util.RECOVERY_CONTACT, acme_util.POP, From 2bd451a9649ba043c82631cd90134a021eb77642 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 30 Mar 2015 17:36:09 -0700 Subject: [PATCH 87/92] fix continuity_auth docs --- docs/api/client/continuity_auth.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/client/continuity_auth.rst b/docs/api/client/continuity_auth.rst index d143a7a79..29f6a3ffb 100644 --- a/docs/api/client/continuity_auth.rst +++ b/docs/api/client/continuity_auth.rst @@ -1,5 +1,5 @@ :mod:`letsencrypt.client.continuity_auth` ----------------------------------------------- +----------------------------------------- .. automodule:: letsencrypt.client.continuity_auth :members: From 162f41d45ef8637a5d5bcf6c1bc9d59a63568112 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 30 Mar 2015 18:18:59 -0700 Subject: [PATCH 88/92] update/cleanup docs --- docs/contributing.rst | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index e3b81b3d4..cf5d95289 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -126,26 +126,27 @@ Installers and Authenticators will oftentimes be the same class/object. Installers and Authenticators are kept separate because it should be possible to use the `~.StandaloneAuthenticator` (it sets up its own Python server to perform challenges) with a program that -cannot solve challenges itself. (I am imagining MTA installers). +cannot solve challenges itself. (Imagine MTA installers). + + +Installer Development +--------------------- + +There are a few existing classes that may be beneficial while +developing a new `~letsencrypt.client.interfaces.IInstaller`. +Installer's aimed to reconfigure UNIX servers may use Augeas for +configuration parsing and can inherit from `~.AugeasConfigurator` class +to handle much of the interface. Installers that are unable to use +Augeas may still use the `~.Reverter` class to handle configuration +checkpoints and rollback. Display ~~~~~~~ -We currently offer a pythondialog and "text" mode for displays. I have -rewritten the interface which should be merged within the next day -(the rewrite is in the revoker branch of the repo and should be merged -within the next day). Display plugins implement -`~letsencrypt.client.interfaces.IDisplay` interface. - - -Augeas ------- - -Some plugins, especially those designed to reconfigure UNIX servers, -can take inherit from `~.AugeasConfigurator` class in order to more -efficiently handle common operations on UNIX server configuration -files. +We currently offer a pythondialog and "text" mode for displays. Display +plugins implement the `~letsencrypt.client.interfaces.IDisplay` +interface. .. _coding-style: From ce3cabfd2f474c6d44cdbd47a7163e55a4eb3e70 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 30 Mar 2015 18:28:36 -0700 Subject: [PATCH 89/92] Fix mistake, rework sentence --- docs/contributing.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index cf5d95289..86d018f46 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -134,11 +134,11 @@ Installer Development There are a few existing classes that may be beneficial while developing a new `~letsencrypt.client.interfaces.IInstaller`. -Installer's aimed to reconfigure UNIX servers may use Augeas for +Installers aimed to reconfigure UNIX servers may use Augeas for configuration parsing and can inherit from `~.AugeasConfigurator` class to handle much of the interface. Installers that are unable to use -Augeas may still use the `~.Reverter` class to handle configuration -checkpoints and rollback. +Augeas may still find the `~.Reverter` class helpful in handling +configuration checkpoints and rollback. Display From dd68d98ac65def7cceeeb51be2555cf64a9b074f Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 31 Mar 2015 11:33:14 -0700 Subject: [PATCH 90/92] Reduce travis noise --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 526b3d33a..26ff9299d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,4 +22,10 @@ env: notifications: email: false - irc: "chat.freenode.net#letsencrypt" + irc: + channels: + - "chat.freenode.net#letsencrypt" + on_success: never + on_failure: always + use_notice: true + skip_join: true From c0dc49b192d5490a77a667e2ece997659ed71a00 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 31 Mar 2015 18:43:20 -0700 Subject: [PATCH 91/92] fix documentation --- letsencrypt/client/auth_handler.py | 4 ++-- letsencrypt/client/continuity_auth.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index 571c51927..8e5020dc2 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -17,11 +17,11 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes """ACME Authorization Handler for a client. :ivar dv_auth: Authenticator capable of solving - :const:`~letsencrypt.acme.challenges.DVChallenge`(s) + :class:`~letsencrypt.acme.challenges.DVChallenge` types :type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` :ivar cont_auth: Authenticator capable of solving - :const:`~letsencrypt.acme.challenges.ContinuityChallenge`(s) + :class:`~letsencrypt.acme.challenges.ContinuityChallenge` types :type cont_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` :ivar network: Network object for sending and receiving authorization diff --git a/letsencrypt/client/continuity_auth.py b/letsencrypt/client/continuity_auth.py index 7603ad166..063d3d408 100644 --- a/letsencrypt/client/continuity_auth.py +++ b/letsencrypt/client/continuity_auth.py @@ -11,7 +11,7 @@ from letsencrypt.client import recovery_token class ContinuityAuthenticator(object): """IAuthenticator for - :const:`~letsencrypt.acme.challenges.ContinuityChallenge`s. + :const:`~letsencrypt.acme.challenges.ContinuityChallenge` class challenges. :ivar rec_token: Performs "recoveryToken" challenges :type rec_token: :class:`letsencrypt.client.recovery_token.RecoveryToken` From 7a4c7acdfb1afa534f310078ad1ffba4ecb60947 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 1 Apr 2015 07:56:38 +0000 Subject: [PATCH 92/92] Fix review comments --- docs/api/acme/index.rst | 2 + letsencrypt/acme/messages2.py | 94 ++++++++++++++--------- letsencrypt/client/network2.py | 46 ++++++++--- letsencrypt/client/tests/network2_test.py | 13 +++- 4 files changed, 105 insertions(+), 50 deletions(-) diff --git a/docs/api/acme/index.rst b/docs/api/acme/index.rst index 9eb93ec6c..20206183a 100644 --- a/docs/api/acme/index.rst +++ b/docs/api/acme/index.rst @@ -1,6 +1,8 @@ :mod:`letsencrypt.acme` ======================= +.. contents:: + .. automodule:: letsencrypt.acme :members: diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index ecb0d9868..f4c1e9dce 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -43,7 +43,8 @@ class Error(jose.JSONObjectWithFields, Exception): return without_prefix @property - def description(self): # pylint: disable=missing-docstring,no-self-argument + def description(self): + """Hardcoded error description based on its type.""" return self.ERROR_TYPE_DESCRIPTIONS[self.typ] @@ -91,7 +92,11 @@ IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder class Identifier(jose.JSONObjectWithFields): - """ACME identifier.""" + """ACME identifier. + + :ivar letsencrypt.acme.messages2.IdentifierType typ: + + """ typ = jose.Field('type', decoder=IdentifierType.from_json) value = jose.Field('value') @@ -99,32 +104,35 @@ class Identifier(jose.JSONObjectWithFields): class Resource(jose.ImmutableMap): """ACME Resource. - :param body: Resource body. - :type body: Instance of `ResourceBody` (subclass). - - :param str uri: Location of the resource. + :ivar letsencrypt.acme.messages2.ResourceBody body: Resource body. + :ivar str uri: Location of the resource. """ __slots__ = ('body', 'uri') class ResourceBody(jose.JSONObjectWithFields): - """ACME Resource body.""" + """ACME Resource Body.""" class RegistrationResource(Resource): - """Registration resource. + """Registration Resource. - :ivar body: `Registration` - :ivar str uri: URI of the resource. - :ivar new_authzr_uri: URI found in the 'next' Link header + :ivar letsencrypt.acme.messages2.Registration body: + :ivar str new_authzr_uri: URI found in the 'next' ``Link`` header + :ivar str terms_of_service: URL for the CA TOS. """ __slots__ = ('body', 'uri', 'new_authzr_uri', 'terms_of_service') class Registration(ResourceBody): - """Registration resource body.""" + """Registration Resource Body. + + :ivar letsencrypt.acme.jose.jwk.JWK key: Public key. + :ivar tuple contact: + + """ # on new-reg key server ignores 'key' and populates it based on # JWS.signature.combined.jwk @@ -135,10 +143,10 @@ class Registration(ResourceBody): class ChallengeResource(Resource, jose.JSONObjectWithFields): - """Challenge resource. + """Challenge Resource. - :ivar body: `.challenges.ChallengeBody` - :ivar authzr_uri: URI found in the 'up' Link header. + :ivar letsencrypt.acme.messages2.ChallengeBody body: + :ivar str authzr_uri: URI found in the 'up' ``Link`` header. """ __slots__ = ('body', 'authzr_uri') @@ -151,18 +159,21 @@ class ChallengeResource(Resource, jose.JSONObjectWithFields): class ChallengeBody(ResourceBody): - """Challenge resource body. - - Confusingly, this has a similar name to `.challenges.Challenge`, as - well as `.achallanges.AnnotatedChallenge` or - `.achallanges.IndexedChallenge`. Use names such as ``challb`` to - distinguish instances of this class from ``achall`` or ``ichall``. + """Challenge Resource Body. .. todo:: - This class could be integrated with challenges.Challenge, but - this way it would be confusing when compared to acme-spec, where - all challenges are presented without 'uri', 'status', or - 'validated' fields. + Confusingly, this has a similar name to `.challenges.Challenge`, + as well as `.achallenges.AnnotatedChallenge` or + `.achallenges.Indexed`... Once `messages2` and `network2` is + integrated with the rest of the client, this class functionality + will be merged with `.challenges.Challenge`. Meanwhile, + separation allows the ``master`` to be still interoperable with + Node.js server (protocol v00). For the time being use names such + as ``challb`` to distinguish instances of this class from + ``achall`` or ``ichall``. + + :ivar letsencrypt.acme.messages2.Status status: + :ivar datetime.datetime validated: """ @@ -184,19 +195,26 @@ class ChallengeBody(ResourceBody): class AuthorizationResource(Resource): - """Authorization resource. + """Authorization Resource. - :ivar body: `Authorization` - :ivar new_cert_uri: URI found in the 'next' Link header + :ivar letsencrypt.acme.messages2.Authorization body: + :ivar str new_cert_uri: URI found in the 'next' ``Link`` header """ __slots__ = ('body', 'uri', 'new_cert_uri') class Authorization(ResourceBody): - """Authorization resource body. + """Authorization Resource Body. - :ivar challenges: `list` of `Challenge` + :ivar letsencrypt.acme.messages2.Identifier identifier: + :ivar list challenges: `list` of `Challenge` + :ivar tuple combinations: Challenge combinations (`tuple` of `tuple` + of `int`, as opposed to `list` of `list` from the spec). + :ivar letsencrypt.acme.jose.jwk.JWK key: Public key. + :ivar tuple contact: + :ivar letsencrypt.acme.messages2.Status status: + :ivar datetime.datetime expires: """ @@ -229,7 +247,9 @@ class Authorization(ResourceBody): class CertificateRequest(jose.JSONObjectWithFields): """ACME new-cert request. - :ivar csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` + :ivar letsencrypt.acme.jose.util.ComparableX509 csr: + `M2Crypto.X509.Request` wrapped in `.ComparableX509` + :ivar tuple authorizations: `tuple` of URIs (`str`) """ csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) @@ -237,11 +257,12 @@ class CertificateRequest(jose.JSONObjectWithFields): class CertificateResource(Resource): - """Authorization resource. + """Certificate Resource. - :ivar body: `M2Crypto.X509.X509` wrapped in `.ComparableX509` - :ivar cert_chain_uri: URI found in the 'up' Link header - :ivar authzrs: `list` of `AuthorizationResource`. + :ivar letsencrypt.acme.jose.util.ComparableX509 body: + `M2Crypto.X509.X509` wrapped in `.ComparableX509` + :ivar str cert_chain_uri: URI found in the 'up' ``Link`` header + :ivar tuple authzrs: `tuple` of `AuthorizationResource`. """ __slots__ = ('body', 'uri', 'cert_chain_uri', 'authzrs') @@ -250,7 +271,8 @@ class CertificateResource(Resource): class Revocation(jose.JSONObjectWithFields): """Revocation message. - :ivar revoke: Either a `datetime.datetime` or `NOW`. + :ivar revoke: Either a `datetime.datetime` or `Revocation.NOW`. + :ivar tuple authorizations: Same as `CertificateRequest.authorizations` """ diff --git a/letsencrypt/client/network2.py b/letsencrypt/client/network2.py index 13c3e8149..c2f535096 100644 --- a/letsencrypt/client/network2.py +++ b/letsencrypt/client/network2.py @@ -23,13 +23,17 @@ requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() class Network(object): """ACME networking. + .. todo:: + Clean up raised error types hierarchy, document, and handle (wrap) + instances of `.DeserializationError` raised in `from_json()``. + :ivar str new_reg_uri: Location of new-reg :ivar key: `.JWK` (private) :ivar alg: `.JWASignature` """ - DER_CONTENT_TYPE = 'application/plix-cert' + DER_CONTENT_TYPE = 'application/pkix-cert' JSON_CONTENT_TYPE = 'application/json' JSON_ERROR_CONTENT_TYPE = 'application/problem+json' @@ -58,6 +62,16 @@ class Network(object): HTTP header is ignored if response is an expected JSON object (c.f. Boulder #56). + :param str content_type: Expected Content-Type response header. + If JSON is expected and not present in server response, this + function will raise an error. Otherwise, wrong Content-Type + is ignored, but logged. + + :raises letsencrypt.messages2.Error: If server response body + carries HTTP Problem (draft-ietf-appsawg-http-problem-00). + :raises letsencrypt.errors.NetworkError: In case of other + networking errors. + """ response_ct = response.headers.get('Content-Type') @@ -222,9 +236,12 @@ class Network(object): :param identifier: Identifier to be challenged. :type identifier: `.messages2.Identifier` - :pram regr: Registration resource. + :param regr: Registration Resource. :type regr: `.RegistrationResource` + :returns: Authorization Resource. + :rtype: `.AuthorizationResource` + """ new_authz = messages2.Authorization(identifier=identifier) response = self._post(regr.new_authzr_uri, self._wrap_in_jws(new_authz)) @@ -232,7 +249,15 @@ class Network(object): return self._authzr_from_response(response, identifier) def request_domain_challenges(self, domain, regr): - """Request challenges for domain names.""" + """Request challenges for domain names. + + This is simply a convenience function that wraps around + `request_challenges`, but works with domain names instead of + generic identifiers. + + :param str domain: Domain name to be challenged. + + """ return self.request_challenges(messages2.Identifier( typ=messages2.IDENTIFIER_FQDN, value=domain), regr) @@ -245,7 +270,7 @@ class Network(object): :param response: Corresponding Challenge response :type response: `.challenges.ChallengeResponse` - :returns: Challenge resource with updated body. + :returns: Challenge Resource with updated body. :rtype: `.ChallengeResource` :raises errors.UnexpectedUpdate: @@ -345,10 +370,7 @@ class Network(object): content_type=content_type, headers={'Accept': content_type}) - try: - cert_chain_uri = response.links['up']['url'] - except KeyError: - raise errors.NetworkError('"up" Link missing') + cert_chain_uri = response.links.get('up', {}).get('url') try: uri = response.headers['Location'] @@ -451,6 +473,9 @@ class Network(object): :rtype: `.CertificateResource` """ + # TODO: If a client sends a refresh request and the server is + # not willing to refresh the certificate, the server MUST + # respond with status code 403 (Forbidden) return self.check_cert(certr) def fetch_chain(self, certr): @@ -459,11 +484,12 @@ class Network(object): :param certr: Certificate Resource :type certr: `.CertificateResource` - :returns: Certificate chain + :returns: Certificate chain, or `None` if no "up" Link was provided. :rtype: `M2Crypto.X509.X509` wrapped in `.ComparableX509` """ - return self._get_cert(certr.cert_chain_uri) + if certr.cert_chain_uri is not None: + return self._get_cert(certr.cert_chain_uri) def revoke(self, certr, when=messages2.Revocation.NOW): """Revoke certificate. diff --git a/letsencrypt/client/tests/network2_test.py b/letsencrypt/client/tests/network2_test.py index d7aa74929..c2a7d877a 100644 --- a/letsencrypt/client/tests/network2_test.py +++ b/letsencrypt/client/tests/network2_test.py @@ -317,13 +317,14 @@ class NetworkTest(unittest.TestCase): # TODO: check POST args def test_request_issuance_missing_up(self): + self.response.content = CERT.as_der() + self.response.headers['Location'] = self.certr.uri self._mock_post_get() - self.assertRaises( - errors.NetworkError, self.net.request_issuance, - CSR, (self.authzr,)) + self.assertEqual( + self.certr.update(cert_chain_uri=None), + self.net.request_issuance(CSR, (self.authzr,))) def test_request_issuance_missing_location(self): - self.response.links['up'] = {'url': self.certr.cert_chain_uri} self._mock_post_get() self.assertRaises( errors.NetworkError, self.net.request_issuance, @@ -437,6 +438,10 @@ class NetworkTest(unittest.TestCase): self.assertEqual(self.net._get_cert(self.certr.cert_chain_uri), self.net.fetch_chain(self.certr)) + def test_fetch_chain_no_up_link(self): + self.assertTrue(self.net.fetch_chain(self.certr.update( + cert_chain_uri=None)) is None) + def test_revoke(self): self._mock_post_get() self.net.revoke(self.certr, when=messages2.Revocation.NOW)