diff --git a/.pylintrc b/.pylintrc index 2af1870c4..fa1c5be45 100644 --- a/.pylintrc +++ b/.pylintrc @@ -38,7 +38,7 @@ load-plugins= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -#disable= +disable=fixme,locally-disabled [REPORTS] @@ -81,7 +81,7 @@ required-attributes= bad-functions=map,filter,apply,input,file # Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ +good-names=i,j,k,ex,Run,_,fd # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata @@ -94,10 +94,10 @@ name-group= include-naming-hint=no # Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ +function-rgx=[a-z_][a-z0-9_]{2,40}$ # Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ +function-name-hint=[a-z_][a-z0-9_]{2,40}$ # Regular expression matching correct variable names variable-rgx=[a-z_][a-z0-9_]{2,30}$ @@ -148,14 +148,14 @@ module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ +method-rgx=[a-z_][a-z0-9_]{2,40}$ # Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ +method-name-hint=[a-z_][a-z0-9_]{2,40}$ # Regular expression which should only match function or class names that do # not require a docstring. -no-docstring-rgx=(__.*__)|(test_[A-Za-z0-9_]*)|(_.*) +no-docstring-rgx=(__.*__)|(test_[A-Za-z0-9_]*)|(_.*)|(.*Test$) # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. @@ -182,7 +182,7 @@ init-import=no # A regular expression matching the name of dummy variables (i.e. expectedly # not used). -dummy-variables-rgx=_$|dummy +dummy-variables-rgx=(unused)?_.*|dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. @@ -278,8 +278,8 @@ int-import-graph= [CLASSES] # List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by +# instance to not check methods defined in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by,implementedBy,providedBy # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp diff --git a/README.md b/README.md index 9f5113931..a8d65d30a 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ In general: ``` sudo apt-get install python python-setuptools python-virtualenv \ - python-dev gcc swig dialog libaugeas0 libssl-dev + python-dev gcc swig dialog libaugeas0 libssl-dev ca-certificates ``` #### Mac OSX @@ -77,7 +77,7 @@ In order to generate the sphinx documentation, run the following commands. ``` ./venv/bin/python setup.py docs cd docs -make html SPHINXBUILD='../venv/bin/python ../venv/bin/sphinx-build' +make clean html SPHINXBUILD=../venv/bin/sphinx-build ``` This should generate documentation in the /lets-encrypt-preview/docs/_build/html diff --git a/docs/_static/.gitignore b/docs/_static/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docs/api/client/auth_handler.rst b/docs/api/client/auth_handler.rst index e84745d1e..b52006993 100644 --- a/docs/api/client/auth_handler.rst +++ b/docs/api/client/auth_handler.rst @@ -1,5 +1,5 @@ :mod:`letsencrypt.client.auth_handler` --------------------------------- +-------------------------------------- .. automodule:: letsencrypt.client.auth_handler :members: diff --git a/docs/api/client/client_authenticator.rst b/docs/api/client/client_authenticator.rst index a9050de50..267a0dd50 100644 --- a/docs/api/client/client_authenticator.rst +++ b/docs/api/client/client_authenticator.rst @@ -1,5 +1,5 @@ :mod:`letsencrypt.client.client_authenticator` --------------------------------- +---------------------------------------------- .. automodule:: letsencrypt.client.client_authenticator :members: diff --git a/letsencrypt/client/CONFIG.py b/letsencrypt/client/CONFIG.py index 23392a045..3188276b9 100644 --- a/letsencrypt/client/CONFIG.py +++ b/letsencrypt/client/CONFIG.py @@ -35,7 +35,7 @@ CERT_KEY_BACKUP = os.path.join(WORK_DIR, "keys-certs/") REV_TOKENS_DIR = os.path.join(WORK_DIR, "revocation_tokens/") """Directory where all revocation tokens are saved.""" -KEY_DIR = os.path.join(SERVER_ROOT, "ssl/") +KEY_DIR = os.path.join(SERVER_ROOT, "keys/") """Where all keys should be stored""" CERT_DIR = os.path.join(SERVER_ROOT, "certs/") diff --git a/letsencrypt/client/apache/configurator.py b/letsencrypt/client/apache/configurator.py index 5602a58fb..88dcb33d1 100644 --- a/letsencrypt/client/apache/configurator.py +++ b/letsencrypt/client/apache/configurator.py @@ -423,7 +423,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "based virtual host", addr) self.add_name_vhost(addr) - def make_vhost_ssl(self, nonssl_vhost): + def make_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals """Makes an ssl_vhost version of a nonssl_vhost. Duplicates vhost and adds default ssl options @@ -519,8 +519,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return ssl_vhost - # pylint: disable=no-method-argument,no-self-use,unused-argument - def supported_enhancements(): + def supported_enhancements(self): # pylint: disable=no-self-use """Returns currently supported enhancements.""" return ["redirect"] @@ -529,7 +528,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param str domain: domain to enhance :param str enhancement: enhancement type defined in - :class:`letsencrypt.client.CONFIG.ENHANCEMENTS + :class:`letsencrypt.client.CONFIG.ENHANCEMENTS` :param options: options for the enhancement :type options: See :class:`letsencrypt.client.CONFIG.ENHANCEMENTS` documentation for appropriate parameter. @@ -544,7 +543,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): except errors.LetsEncryptConfiguratorError: logging.warn("Failed %s for %s", enhancement, domain) - def _enable_redirect(self, ssl_vhost, options): + 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 @@ -559,8 +558,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :param ssl_vhost: Destination of traffic, an ssl enabled vhost :type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost` - :param options: Not currently used - :type options: Not Available + :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`) @@ -886,7 +885,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ return apache_restart() - def config_test(self): + def config_test(self): # pylint: disable=no-self-use """Check the configuration of Apache for errors. :returns: Success @@ -976,7 +975,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): fulfilled by configurator. :returns: list of responses. All responses are returned in the same - order as received by the perform function. A None response + order as received by the perform function. A None response indicates the challenge was not perfromed. :rtype: list diff --git a/letsencrypt/client/apache/dvsni.py b/letsencrypt/client/apache/dvsni.py index 21012c17a..cf5c3bdb0 100644 --- a/letsencrypt/client/apache/dvsni.py +++ b/letsencrypt/client/apache/dvsni.py @@ -17,7 +17,7 @@ class ApacheDvsni(object): :ivar dvsni_chall: Data required for challenges. where DvsniChall tuples have the following fields `domain` (`str`), `r_b64` (base64 `str`), `nonce` (hex `str`) - `key` (:class:`letsencrypt.client.client.Client.Key`) + `key` (:class:`letsencrypt.client.client.Client.Key`) :type dvsni_chall: `list` of :class:`letsencrypt.client.challenge_util.DvsniChall` diff --git a/letsencrypt/client/apache/obj.py b/letsencrypt/client/apache/obj.py index df2f36ec4..647724a48 100644 --- a/letsencrypt/client/apache/obj.py +++ b/letsencrypt/client/apache/obj.py @@ -2,7 +2,7 @@ class Addr(object): - """Represents an Apache VirtualHost address. + r"""Represents an Apache VirtualHost address. :param str addr: addr part of vhost address :param str port: port number or \*, or "" @@ -58,7 +58,7 @@ class VirtualHost(object): """ - def __init__(self, filep, path, addrs, ssl, enabled, names=None): + def __init__(self, filep, path, addrs, ssl, enabled, names=None): # pylint: disable=too-many-arguments """Initialize a VH.""" self.filep = filep self.path = path diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index f33aede1c..b85996818 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -8,7 +8,7 @@ from letsencrypt.client import challenge_util from letsencrypt.client import errors -class AuthHandler(object): +class AuthHandler(object): # pylint: disable=too-many-instance-attributes """ACME Authorization Handler for a client. :ivar dv_auth: Authenticator capable of solving CONFIG.DV_CHALLENGES @@ -286,7 +286,7 @@ class AuthHandler(object): raise errors.LetsEncryptClientError( "Unimplemented Auth Challenge: %s" % chall["type"]) - def _construct_client_chall(self, chall, domain): + def _construct_client_chall(self, chall, domain): # pylint: disable=no-self-use """Construct Client Type Challenges. :param dict chall: Single challenge diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index 8508546f7..7089cada6 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -226,6 +226,7 @@ def get_cert_info(filename): .. todo:: Pub key is assumed to be RSA... find a good solution to allow EC. :param str filename: Name of file containing certificate in PEM format. + :rtype: dict """ @@ -233,8 +234,8 @@ def get_cert_info(filename): cert = M2Crypto.X509.load_cert(filename) try: - san = cert.get_ext('subjectAltName').get_value() - except: + san = cert.get_ext("subjectAltName").get_value() + except LookupError: san = "" return { @@ -251,5 +252,6 @@ def get_cert_info(filename): def b64_cert_to_pem(b64_der_cert): + """Convert JOSE Base-64 encoded DER cert to PEM.""" return M2Crypto.X509.load_cert_der_string( le_util.jose_b64decode(b64_der_cert)).as_pem() diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index cc929f331..26d52256b 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -1,3 +1,4 @@ +"""Lets Encrypt display.""" import textwrap import dialog @@ -11,10 +12,10 @@ WIDTH = 72 HEIGHT = 20 -class CommonDisplayMixin(object): - """methods common to both NcursesDisplay and FileDisplay""" +class CommonDisplayMixin(object): # pylint: disable=too-few-public-methods + """Mixin with methods common to classes implementing IDisplay.""" - def redirect_by_default(self): + def redirect_by_default(self): # pylint: disable=missing-docstring choices = [ ("Easy", "Allow both HTTP and HTTPS access to these sites"), ("Secure", "Make all requests redirect to secure HTTPS access")] @@ -31,6 +32,8 @@ class CommonDisplayMixin(object): class NcursesDisplay(CommonDisplayMixin): + """Ncurses-based display.""" + zope.interface.implements(interfaces.IDisplay) def __init__(self, width=WIDTH, height=HEIGHT): @@ -39,10 +42,10 @@ class NcursesDisplay(CommonDisplayMixin): self.width = width self.height = height - def generic_notification(self, message): + def generic_notification(self, message): # pylint: disable=missing-docstring self.dialog.msgbox(message, width=self.width) - def generic_menu(self, message, choices, input_text=""): + def generic_menu(self, message, choices, unused_input_text=""): # pylint: disable=missing-docstring # Can accept either tuples or just the actual choices if choices and isinstance(choices[0], tuple): code, selection = self.dialog.menu( @@ -55,26 +58,27 @@ class NcursesDisplay(CommonDisplayMixin): return code(int(tag) - 1) - def generic_input(self, message): + def generic_input(self, message): # pylint: disable=missing-docstring return self.dialog.inputbox(message) - def generic_yesno(self, message, yes="Yes", no="No"): + def generic_yesno(self, message, yes_label="Yes", no_label="No"): # pylint: disable=missing-docstring return self.dialog.DIALOG_OK == self.dialog.yesno( - message, self.height, self.width, yes_label=yes, no_label=no) + message, self.height, self.width, + yes_label=yes_label, no_label=no_label) - def filter_names(self, names): + def filter_names(self, names): # pylint: disable=missing-docstring choices = [(n, "", 0) for n in names] code, names = self.dialog.checklist( "Which names would you like to activate HTTPS for?", choices=choices) return code, [str(s) for s in names] - def success_installation(self, domains): + def success_installation(self, domains): # pylint: disable=missing-docstring self.dialog.msgbox( "\nCongratulations! You have successfully enabled " + gen_https_names(domains) + "!", width=self.width) - def display_certs(self, certs): + def display_certs(self, certs): # pylint: disable=missing-docstring list_choices = [] for i, c in enumerate(certs): if c['installed']: @@ -99,7 +103,7 @@ class NcursesDisplay(CommonDisplayMixin): tag = -1 return code, (int(tag) - 1) - def confirm_revocation(self, cert): + def confirm_revocation(self, cert): # pylint: disable=missing-docstring text = ("Are you sure you would like to revoke the following " "certificate:\n") text += cert_info_frame(cert) @@ -107,7 +111,7 @@ class NcursesDisplay(CommonDisplayMixin): return self.dialog.DIALOG_OK == self.dialog.yesno( text, width=self.width, height=self.height) - def more_info_cert(self, cert): + def more_info_cert(self, cert): # pylint: disable=missing-docstring text = "Certificate Information:\n" text += cert_info_frame(cert) print text @@ -115,20 +119,21 @@ class NcursesDisplay(CommonDisplayMixin): class FileDisplay(CommonDisplayMixin): + """File-based display.""" + zope.interface.implements(interfaces.IDisplay) def __init__(self, outfile): super(FileDisplay, self).__init__() self.outfile = outfile - def generic_notification(self, message): + def generic_notification(self, message): # pylint: disable=missing-docstring side_frame = '-' * 79 - wm = textwrap.fill(message, 80) - text = "\n%s\n%s\n%s\n" % (side_frame, wm, side_frame) - self.outfile.write(text) + msg = textwrap.fill(message, 80) + self.outfile.write("\n%s\n%s\n%s\n" % (side_frame, msg, side_frame)) raw_input("Press Enter to Continue") - def generic_menu(self, message, choices, input_text=""): + def generic_menu(self, message, choices, input_text=""): # pylint: disable=missing-docstring # Can take either tuples or single items in choices list if choices and isinstance(choices[0], tuple): choices = ["%s - %s" % (c[0], c[1]) for c in choices] @@ -148,7 +153,7 @@ class FileDisplay(CommonDisplayMixin): return code, (selection - 1) - def generic_input(self, message): + def generic_input(self, message): # pylint: disable=no-self-use,missing-docstring ans = raw_input("%s (Enter c to cancel)\n" % message) if ans.startswith('c') or ans.startswith('C'): @@ -156,12 +161,12 @@ class FileDisplay(CommonDisplayMixin): else: return OK, ans - def generic_yesno(self, message, yes_label="Yes", no_label="No"): + def generic_yesno(self, message, unused_yes_label="", unused_no_label=""): # pylint: disable=missing-docstring self.outfile.write("\n%s\n" % textwrap.fill(message, 80)) ans = raw_input("y/n: ") return ans.startswith('y') or ans.startswith('Y') - def filter_names(self, names): + def filter_names(self, names): # pylint: disable=missing-docstring code, tag = self.generic_menu( "Choose the names would you like to upgrade to HTTPS?", names, "Select the number of the name: ") @@ -169,7 +174,7 @@ class FileDisplay(CommonDisplayMixin): # Make sure to return a list... return code, [names[tag]] - def display_certs(self, certs): + def display_certs(self, certs): # pylint: disable=missing-docstring menu_choices = [(str(i+1), str(c["cn"]) + " - " + c["pub_key"] + " - " + str(c["not_before"])[:-6]) for i, c in enumerate(certs)] @@ -206,14 +211,13 @@ class FileDisplay(CommonDisplayMixin): return code, selection - def success_installation(self, domains): - s_f = '*' * 79 - wm = textwrap.fill(("Congratulations! You have successfully " + - "enabled %s!") % gen_https_names(domains)) - msg = "%s\n%s\n%s\n" - self.outfile.write(msg % (s_f, wm, s_f)) + def success_installation(self, domains): # pylint: disable=missing-docstring + side_frame = '*' * 79 + msg = textwrap.fill("Congratulations! You have successfully " + "enabled %s!" % gen_https_names(domains)) + self.outfile.write("%s\n%s\n%s\n" % (side_frame, msg, side_frame)) - def confirm_revocation(self, cert): + def confirm_revocation(self, cert): # pylint: disable=missing-docstring self.outfile.write("Are you sure you would like to revoke " "the following certificate:\n") self.outfile.write(cert_info_frame(cert)) @@ -221,7 +225,7 @@ class FileDisplay(CommonDisplayMixin): ans = raw_input("y/n") return ans.startswith('y') or ans.startswith('Y') - def more_info_cert(self, cert): + def more_info_cert(self, cert): # pylint: disable=missing-docstring self.outfile.write("\nCertificate Information:\n") self.outfile.write(cert_info_frame(cert)) @@ -230,14 +234,14 @@ CANCEL = "cancel" HELP = "help" -def cert_info_frame(cert): +def cert_info_frame(cert): # pylint: disable=missing-docstring text = "-" * (WIDTH - 4) + "\n" text += cert_info_string(cert) text += "-" * (WIDTH - 4) return text -def cert_info_string(cert): +def cert_info_string(cert): # pylint: disable=missing-docstring text = "Subject: %s\n" % cert["subject"] text += "SAN: %s\n" % cert["san"] text += "Issuer: %s\n" % cert["issuer"] diff --git a/letsencrypt/client/interactive_challenge.py b/letsencrypt/client/interactive_challenge.py index c802ca191..4130525f5 100644 --- a/letsencrypt/client/interactive_challenge.py +++ b/letsencrypt/client/interactive_challenge.py @@ -1,3 +1,4 @@ +"""Interactive challenge.""" import textwrap import dialog @@ -23,7 +24,7 @@ class InteractiveChallenge(object): super(InteractiveChallenge, self).__init__() self.string = string - def perform(self, quiet=True): + def perform(self, quiet=True): # pylint: disable=missing-docstring if quiet: dialog.Dialog().msgbox( self.get_display_string(), width=self.BOX_SIZE) @@ -33,7 +34,7 @@ class InteractiveChallenge(object): return True - def get_display_string(self): + def get_display_string(self): # pylint: disable=missing-docstring return (textwrap.fill(self.string, width=self.BOX_SIZE) + "\n\nPlease Press Enter to Continue") diff --git a/letsencrypt/client/le_util.py b/letsencrypt/client/le_util.py index 08b0f6114..59b581a45 100644 --- a/letsencrypt/client/le_util.py +++ b/letsencrypt/client/le_util.py @@ -49,25 +49,24 @@ def check_permissions(filepath, mode, uid=0): return stat.S_IMODE(file_stat.st_mode) == mode and file_stat.st_uid == uid -def unique_file(default_name, mode=0o777): +def unique_file(path, mode=0o777): """Safely finds a unique file for writing only (by default). - :param str default_name: Default file name + :param str path: path/filename.ext :param int mode: File mode :return: tuple of file object and file name """ - count = 1 - f_parsed = os.path.splitext(default_name) - while 1: + path, tail = os.path.split(path) + count = 0 + while True: + fname = os.path.join(path, "%04d_%s" % (count, tail)) try: - file_d = os.open( - default_name, os.O_CREAT | os.O_EXCL | os.O_RDWR, mode) - return os.fdopen(file_d, 'w'), default_name + file_d = os.open(fname, os.O_CREAT | os.O_EXCL | os.O_RDWR, mode) + return os.fdopen(file_d, 'w'), fname except OSError: pass - default_name = f_parsed[0] + '_' + str(count) + f_parsed[1] count += 1 diff --git a/letsencrypt/client/recovery_contact_challenge.py b/letsencrypt/client/recovery_contact_challenge.py index 6bafed829..6cfab00d0 100644 --- a/letsencrypt/client/recovery_contact_challenge.py +++ b/letsencrypt/client/recovery_contact_challenge.py @@ -30,8 +30,8 @@ class RecoveryContact(object): self.contact = contact self.poll_delay = poll_delay - def perform(self, quiet=True): - d = dialog.Dialog() + def perform(self, quiet=True): # pylint: disable=missing-docstring + d = dialog.Dialog() # pylint: disable=invalid-name if quiet: if self.success_url: d.infobox(self.get_display_string()) @@ -50,7 +50,7 @@ class RecoveryContact(object): return True - def cleanup(self): + def cleanup(self): # pylint: disable=no-self-use,missing-docstring return def get_display_string(self): @@ -91,7 +91,7 @@ class RecoveryContact(object): else: return False - def prompt_continue(self, quiet=True): + def prompt_continue(self, quiet=True): # pylint: disable=no-self-use """Prompt user for continuation. :param bool quiet: Display dialog box if True, raw prompt otherwise. @@ -109,7 +109,7 @@ class RecoveryContact(object): return ans.startswith('y') or ans.startswith('Y') - def generate_response(self): + def generate_response(self): # pylint: disable=missing-docstring if not self.token: return {"type": "recoveryContact"} return { diff --git a/letsencrypt/client/revoker.py b/letsencrypt/client/revoker.py index 4629b7e3f..08236e713 100644 --- a/letsencrypt/client/revoker.py +++ b/letsencrypt/client/revoker.py @@ -57,12 +57,13 @@ class Revoker(object): return c_sha1_vh = {} + if self.installer is not None: for (cert, _, path) in self.installer.get_all_certs_keys(): try: c_sha1_vh[M2Crypto.X509.load_cert( cert).get_fingerprint(md='sha1')] = path - except: + except M2Crypto.X509.X509Error: continue with open(list_file, 'rb') as csvfile: diff --git a/letsencrypt/client/tests/acme_test.py b/letsencrypt/client/tests/acme_test.py index 808eefc1b..f3cf4a69a 100644 --- a/letsencrypt/client/tests/acme_test.py +++ b/letsencrypt/client/tests/acme_test.py @@ -45,7 +45,8 @@ class ACMEObjectValidateTest(unittest.TestCase): class PrettyTest(unittest.TestCase): """Tests for letsencrypt.client.acme.pretty.""" - def _call(self, json_string): + @classmethod + def _call(cls, json_string): from letsencrypt.client.acme import pretty return pretty(json_string) @@ -64,31 +65,19 @@ class MessageFactoriesTest(unittest.TestCase): self.nonce = '\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9' self.b64nonce = '7Nbyb1lI6xPVI3Hg3aKSqQ' - def _validate(self, msg): + @classmethod + def _validate(cls, msg): from letsencrypt.client.acme import SCHEMATA jsonschema.validate(msg, SCHEMATA[msg['type']]) - def _signature(self, sig): - return { - 'nonce': self.b64nonce, - 'alg': 'RS256', - 'jwk': { - 'kty': 'RSA', - 'e': 'AQAB', - 'n': 'rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5' - '80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q', - }, - 'sig': sig, - } - def test_challenge_request(self): from letsencrypt.client.acme import challenge_request msg = challenge_request('example.com') + self._validate(msg) self.assertEqual(msg, { 'type': 'challengeRequest', 'identifier': 'example.com', }) - self._validate(msg) def test_authorization_request(self): from letsencrypt.client.acme import authorization_request @@ -112,51 +101,57 @@ class MessageFactoriesTest(unittest.TestCase): self.nonce, ) + self._validate(msg) + self.assertEqual( + msg.pop('signature')['sig'], + 'VkpReso87ogwGul2MGck96TkYs4QoblIgNthgrm9O7EBGlzCRCnTHnx' + 'bj6loqaC4f5bn1rgS927Gp1Kvbqnmqg' + ) self.assertEqual(msg, { 'type': 'authorizationRequest', 'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4', 'nonce': 'czpsrF0KMH6dgajig3TGHw', - 'signature': self._signature( - 'VkpReso87ogwGul2MGck96TkYs4QoblIgNthgrm9O7EBGlzCRCnTHnx' - 'bj6loqaC4f5bn1rgS927Gp1Kvbqnmqg'), 'responses': responses, }) - self._validate(msg) def test_certificate_request(self): from letsencrypt.client.acme import certificate_request msg = certificate_request( 'TODO: real DER CSR?', self.privkey, self.nonce) + self._validate(msg) + self.assertEqual( + msg.pop('signature')['sig'], + 'HEQVN4MU1yDrArP2T7WZQ12XlHCn5DgTPgb5eWT5_vjRPppLSNe6uWE' + 'x9SFwG9d9umqn49nZCSW7uskA2lcW6Q' + ) self.assertEqual(msg, { 'type': 'certificateRequest', 'csr': 'VE9ETzogcmVhbCBERVIgQ1NSPw', - 'signature': self._signature( - 'HEQVN4MU1yDrArP2T7WZQ12XlHCn5DgTPgb5eWT5_vjRPppLSNe6uWE' - 'x9SFwG9d9umqn49nZCSW7uskA2lcW6Q'), }) - self._validate(msg) def test_revocation_request(self): from letsencrypt.client.acme import revocation_request msg = revocation_request( 'TODO: real DER cert?', self.privkey, self.nonce) + self._validate(msg) + self.assertEqual( + msg.pop('signature')['sig'], + 'ABXA1IsyTalTXIojxmGnIUGyZASmvqEvTQ98jJ5KFs2FTswLEmsoqFX' + 'fU6l5_fous-tsbXOfLN-7PjfZ5XWPvg' + ) self.assertEqual(msg, { 'type': 'revocationRequest', 'certificate': 'VE9ETzogcmVhbCBERVIgY2VydD8', - 'signature': self._signature( - 'ABXA1IsyTalTXIojxmGnIUGyZASmvqEvTQ98jJ5KFs2FTswLEmsoqFX' - 'fU6l5_fous-tsbXOfLN-7PjfZ5XWPvg'), }) - self._validate(msg) def test_status_request(self): from letsencrypt.client.acme import status_request msg = status_request(u'O7-s9MNq1siZHlgrMzi9_A') + self._validate(msg) self.assertEqual(msg, { 'type': 'statusRequest', 'token': u'O7-s9MNq1siZHlgrMzi9_A', }) - self._validate(msg) if __name__ == '__main__': diff --git a/letsencrypt/client/tests/apache/configurator_test.py b/letsencrypt/client/tests/apache/configurator_test.py index a4946a6be..749ecb9c0 100644 --- a/letsencrypt/client/tests/apache/configurator_test.py +++ b/letsencrypt/client/tests/apache/configurator_test.py @@ -1,6 +1,5 @@ """Test for letsencrypt.client.apache.configurator.""" import os -import pkg_resources import re import shutil import unittest @@ -15,30 +14,19 @@ from letsencrypt.client.apache import configurator from letsencrypt.client.apache import obj from letsencrypt.client.apache import parser -from letsencrypt.client.tests.apache import config_util +from letsencrypt.client.tests.apache import util -class TwoVhost80Test(unittest.TestCase): +class TwoVhost80Test(util.ApacheTest): """Test two standard well configured HTTP vhosts.""" def setUp(self): - self.temp_dir, self.config_dir, self.work_dir = config_util.dir_setup( - "debian_apache_2_4/two_vhost_80") + super(TwoVhost80Test, self).setUp() - self.ssl_options = config_util.setup_apache_ssl_options(self.config_dir) + self.config = util.get_apache_configurator( + self.config_path, self.config_dir, self.work_dir, self.ssl_options) - # Final slash is currently important - self.config_path = os.path.join( - self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2/") - - with mock.patch("letsencrypt.client.apache.configurator." - "mod_loaded") as mock_load: - mock_load.return_value = True - self.config = config_util.get_apache_configurator( - self.config_path, self.config_dir, self.work_dir, - self.ssl_options) - - self.vh_truth = config_util.get_vh_truth( + self.vh_truth = util.get_vh_truth( self.temp_dir, "debian_apache_2_4/two_vhost_80") def tearDown(self): @@ -172,12 +160,7 @@ class TwoVhost80Test(unittest.TestCase): def test_perform(self, mock_restart, mock_dvsni_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded - rsa256_file = pkg_resources.resource_filename( - "letsencrypt.client.tests", 'testdata/rsa256_key.pem') - rsa256_pem = pkg_resources.resource_string( - "letsencrypt.client.tests", 'testdata/rsa256_key.pem') - - auth_key = client.Client.Key(rsa256_file, rsa256_pem) + auth_key = client.Client.Key(self.rsa256_file, self.rsa256_pem) chall1 = challenge_util.DvsniChall( "encryption-example.demo", "jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q", diff --git a/letsencrypt/client/tests/apache/dvsni_test.py b/letsencrypt/client/tests/apache/dvsni_test.py index 1bd55ac61..feaec124f 100644 --- a/letsencrypt/client/tests/apache/dvsni_test.py +++ b/letsencrypt/client/tests/apache/dvsni_test.py @@ -1,5 +1,4 @@ """Test for letsencrypt.client.apache.dvsni.""" -import os import pkg_resources import unittest import shutil @@ -10,30 +9,19 @@ from letsencrypt.client import challenge_util from letsencrypt.client import client from letsencrypt.client import CONFIG -from letsencrypt.client.tests.apache import config_util +from letsencrypt.client.tests.apache import util -class DvsniPerformTest(unittest.TestCase): +class DvsniPerformTest(util.ApacheTest): """Test the ApacheDVSNI challenge.""" + def setUp(self): + super(DvsniPerformTest, self).setUp() + + config = util.get_apache_configurator( + self.config_path, self.config_dir, self.work_dir, self.ssl_options) + from letsencrypt.client.apache import dvsni - - self.temp_dir, self.config_dir, self.work_dir = config_util.dir_setup( - "debian_apache_2_4/two_vhost_80") - - self.ssl_options = config_util.setup_apache_ssl_options(self.config_dir) - - # Final slash is currently important - self.config_path = os.path.join( - self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2/") - - with mock.patch("letsencrypt.client.apache.configurator." - "mod_loaded") as mock_load: - mock_load.return_value = True - config = config_util.get_apache_configurator( - self.config_path, self.config_dir, self.work_dir, - self.ssl_options) - self.sni = dvsni.ApacheDvsni(config) rsa256_file = pkg_resources.resource_filename( @@ -148,3 +136,7 @@ class DvsniPerformTest(unittest.TestCase): self.assertEqual( vhost.names, set([str(self.challs[1].nonce + CONFIG.INVALID_EXT)])) + + +if __name__ == '__main__': + unittest.main() diff --git a/letsencrypt/client/tests/apache/parser_test.py b/letsencrypt/client/tests/apache/parser_test.py index 2acf6533e..df0046ff5 100644 --- a/letsencrypt/client/tests/apache/parser_test.py +++ b/letsencrypt/client/tests/apache/parser_test.py @@ -11,23 +11,18 @@ import zope.component from letsencrypt.client import display from letsencrypt.client import errors from letsencrypt.client.apache import parser -from letsencrypt.client.tests.apache import config_util + +from letsencrypt.client.tests.apache import util -class ApacheParserTest(unittest.TestCase): +class ApacheParserTest(util.ApacheTest): """Apache Parser Test.""" + def setUp(self): + super(ApacheParserTest, self).setUp() + zope.component.provideUtility(display.FileDisplay(sys.stdout)) - self.temp_dir, self.config_dir, self.work_dir = config_util.dir_setup( - "debian_apache_2_4/two_vhost_80") - - self.ssl_options = config_util.setup_apache_ssl_options(self.config_dir) - - # Final slash is currently important - self.config_path = os.path.join( - self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2/") - self.parser = parser.ApacheParser( augeas.Augeas(flags=augeas.Augeas.NONE), self.config_path, self.ssl_options) @@ -112,3 +107,7 @@ class ApacheParserTest(unittest.TestCase): self.assertEqual(results["default"], results["listen"]) self.assertEqual(results["default"], results["name"]) + + +if __name__ == '__main__': + unittest.main() diff --git a/letsencrypt/client/tests/apache/config_util.py b/letsencrypt/client/tests/apache/util.py similarity index 81% rename from letsencrypt/client/tests/apache/config_util.py rename to letsencrypt/client/tests/apache/util.py index ad38818ab..d5a662924 100644 --- a/letsencrypt/client/tests/apache/config_util.py +++ b/letsencrypt/client/tests/apache/util.py @@ -1,7 +1,9 @@ +"""Common utilities for letsencrypt.client.apache.""" import os import pkg_resources import shutil import tempfile +import unittest import mock @@ -10,6 +12,26 @@ from letsencrypt.client.apache import configurator from letsencrypt.client.apache import obj +class ApacheTest(unittest.TestCase): + + def setUp(self): + super(ApacheTest, self).setUp() + + self.temp_dir, self.config_dir, self.work_dir = dir_setup( + "debian_apache_2_4/two_vhost_80") + + self.ssl_options = setup_apache_ssl_options(self.config_dir) + + # Final slash is currently important + self.config_path = os.path.join( + 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') + self.rsa256_pem = pkg_resources.resource_string( + "letsencrypt.client.tests", 'testdata/rsa256_key.pem') + + def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"): """Setup the directories necessary for the configurator.""" temp_dir = tempfile.mkdtemp("temp") diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index 2cb801efc..c5e97dace 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -1,21 +1,26 @@ -"""Test auth_handler.py.""" +"""Tests for letsencrypt.client.auth_handler.""" import unittest + import mock +from letsencrypt.client import errors from letsencrypt.client.tests import acme_util -TRANSLATE = {"dvsni": "DvsniChall", - "simpleHttps": "SimpleHttpsChall", - "dns": "DnsChall", - "recoveryToken": "RecTokenChall", - "recoveryContact": "RecContactChall", - "proofOfPossession": "PopChall"} +TRANSLATE = { + "dvsni": "DvsniChall", + "simpleHttps": "SimpleHttpsChall", + "dns": "DnsChall", + "recoveryToken": "RecTokenChall", + "recoveryContact": "RecContactChall", + "proofOfPossession": "PopChall", +} # pylint: disable=protected-access class SatisfyChallengesTest(unittest.TestCase): """verify_identities test.""" + def setUp(self): from letsencrypt.client.auth_handler import AuthHandler @@ -225,7 +230,7 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual( type(self.handler.client_c["4"][0].chall).__name__, "RecTokenChall") - def _get_exp_response(self, domain, path, challenges): + def _get_exp_response(self, domain, path, challenges): # pylint: disable=no-self-use exp_resp = ["null"] * len(challenges) for i in path: exp_resp[i] = TRANSLATE[challenges[i]["type"]] + str(domain) @@ -283,7 +288,6 @@ class GetAuthorizationsTest(unittest.TestCase): self.handler.dv_c[dom], self.handler.client_c[dom] = dv_c, c_c def test_progress_failure(self): - from letsencrypt.client.errors import LetsEncryptAuthHandlerError challenges = acme_util.get_challenges() self.handler.add_chall_msg( "0", @@ -294,7 +298,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_sat_chall.side_effect = self._sat_failure self.assertRaises( - LetsEncryptAuthHandlerError, self.handler.get_authorizations) + errors.LetsEncryptAuthHandlerError, self.handler.get_authorizations) # Check to make sure program didn't loop self.assertEqual(self.mock_sat_chall.call_count, 1) @@ -327,8 +331,6 @@ class GetAuthorizationsTest(unittest.TestCase): [mock.call("1"), mock.call("0")]) def _sat_incremental(self): - from letsencrypt.client.errors import LetsEncryptAuthHandlerError - # Exact responses don't matter, just path/response match if self.iteration == 0: # Only solve one of "0" required challs @@ -353,7 +355,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.handler.responses["0"][3] = "finally!" else: - raise LetsEncryptAuthHandlerError( + raise errors.LetsEncryptAuthHandlerError( "Failed incremental test: too many invocations") def _test_finished(self): @@ -408,12 +410,12 @@ class PathSatisfiedTest(unittest.TestCase): self.assertFalse(self.handler._path_satisfied(dom[i])) -def gen_auth_resp(chall_list): +def gen_auth_resp(chall_list): # pylint: disable=missing-docstring return ["%s%s" % (type(chall).__name__, chall.domain) for chall in chall_list] -def gen_path(str_list, challenges): +def gen_path(str_list, challenges): # pylint: disable=missing-docstring path = [] for i, chall in enumerate(challenges): for str_chall in str_list: diff --git a/letsencrypt/client/tests/crypto_util_test.py b/letsencrypt/client/tests/crypto_util_test.py index 3e943e898..96acdbd9b 100644 --- a/letsencrypt/client/tests/crypto_util_test.py +++ b/letsencrypt/client/tests/crypto_util_test.py @@ -30,7 +30,8 @@ class CreateSigTest(unittest.TestCase): 'AX9AFJHk-bCMQPJbSzXKjG6H1IWbvxjS2Ew', } - def _call(self, *args, **kwargs): + @classmethod + def _call(cls, *args, **kwargs): from letsencrypt.client.crypto_util import create_sig return create_sig(*args, **kwargs) @@ -50,7 +51,8 @@ class CreateSigTest(unittest.TestCase): class ValidCSRTest(unittest.TestCase): """Tests for letsencrypt.client.crypto_util.valid_csr.""" - def _call(self, csr): + @classmethod + def _call(cls, csr): from letsencrypt.client.crypto_util import valid_csr return valid_csr(csr) @@ -80,7 +82,8 @@ class ValidCSRTest(unittest.TestCase): class CSRMatchesPubkeyTest(unittest.TestCase): """Tests for letsencrypt.client.crypto_util.csr_matches_pubkey.""" - def _call_testdata(self, name, privkey): + @classmethod + def _call_testdata(cls, name, privkey): from letsencrypt.client.crypto_util import csr_matches_pubkey return csr_matches_pubkey(pkg_resources.resource_string( __name__, os.path.join('testdata', name)), privkey) @@ -95,7 +98,7 @@ class CSRMatchesPubkeyTest(unittest.TestCase): class MakeKeyTest(unittest.TestCase): """Tests for letsencrypt.client.crypto_util.make_key.""" - def test_it(self): + def test_it(self): # pylint: disable=no-self-use from letsencrypt.client.crypto_util import make_key M2Crypto.RSA.load_key_string(make_key(1024)) M2Crypto.RSA.load_key_string(make_key(2048)) @@ -105,7 +108,8 @@ class MakeKeyTest(unittest.TestCase): class ValidPrivkeyTest(unittest.TestCase): """Tests for letsencrypt.client.crypto_util.valid_privkey.""" - def _call(self, privkey): + @classmethod + def _call(cls, privkey): from letsencrypt.client.crypto_util import valid_privkey return valid_privkey(privkey) @@ -122,7 +126,7 @@ class ValidPrivkeyTest(unittest.TestCase): class MakeSSCertTest(unittest.TestCase): """Tests for letsencrypt.client.crypto_util.make_ss_cert.""" - def test_it(self): + def test_it(self): # pylint: disable=no-self-use from letsencrypt.client.crypto_util import make_ss_cert make_ss_cert(RSA256_KEY, ['example.com', 'www.example.com']) diff --git a/letsencrypt/client/tests/le_util_test.py b/letsencrypt/client/tests/le_util_test.py index f6c58ac0b..5cc71a1ef 100644 --- a/letsencrypt/client/tests/le_util_test.py +++ b/letsencrypt/client/tests/le_util_test.py @@ -100,26 +100,25 @@ class UniqueFileTest(unittest.TestCase): self.assertEqual(0o700, os.stat(self._call(0o700)[1]).st_mode & 0o777) self.assertEqual(0o100, os.stat(self._call(0o100)[1]).st_mode & 0o777) - def test_default_not_exists(self): - self.assertEqual(self._call()[1], self.default_name) - def test_default_exists(self): - name1 = self._call()[1] # create foo.txt + name1 = self._call()[1] # create 0000_foo.txt name2 = self._call()[1] name3 = self._call()[1] self.assertNotEqual(name1, name2) - basename2 = os.path.basename(name2) - self.assertEqual(os.path.dirname(name2), self.root_path) - self.assertTrue(basename2.startswith('foo')) - self.assertTrue(basename2.endswith('.txt')) - self.assertNotEqual(name1, name3) self.assertNotEqual(name2, name3) - basename3 = os.path.basename(name3) + + self.assertEqual(os.path.dirname(name1), self.root_path) + self.assertEqual(os.path.dirname(name2), self.root_path) self.assertEqual(os.path.dirname(name3), self.root_path) - self.assertTrue(basename3.startswith('foo')) - self.assertTrue(basename3.endswith('.txt')) + + basename1 = os.path.basename(name2) + self.assertTrue(basename1.endswith('foo.txt')) + basename2 = os.path.basename(name2) + self.assertTrue(basename2.endswith('foo.txt')) + basename3 = os.path.basename(name3) + self.assertTrue(basename3.endswith('foo.txt')) # https://en.wikipedia.org/wiki/Base64#Examples @@ -141,7 +140,8 @@ B64_URL_UNSAFE_EXAMPLES = { class JOSEB64EncodeTest(unittest.TestCase): """Tests for letsencrypt.client.le_util.jose_b64encode.""" - def _call(self, data): + @classmethod + def _call(cls, data): from letsencrypt.client.le_util import jose_b64encode return jose_b64encode(data) @@ -160,7 +160,8 @@ class JOSEB64EncodeTest(unittest.TestCase): class JOSEB64DecodeTest(unittest.TestCase): """Tests for letsencrypt.client.le_util.jose_b64decode.""" - def _call(self, data): + @classmethod + def _call(cls, data): from letsencrypt.client.le_util import jose_b64decode return jose_b64decode(data) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 9cb8356a7..37673a0c5 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -6,6 +6,7 @@ import os import sys import zope.component +import zope.interface from letsencrypt.client import CONFIG from letsencrypt.client import client @@ -18,7 +19,7 @@ from letsencrypt.client import revoker from letsencrypt.client.apache import configurator -def main(): +def main(): # pylint: disable=too-many-statements """Command line argument parsing and main script execution.""" if not os.geteuid() == 0: sys.exit( @@ -41,7 +42,7 @@ def main(): help="Revert configuration N number of checkpoints.") parser.add_argument("-B", "--keysize", dest="key_size", type=int, default=CONFIG.RSA_KEY_SIZE, metavar="N", - help="RSA key shall be sized N bits. [%(default)s]") + help="RSA key shall be sized N bits. [%(default)d]") parser.add_argument("-k", "--revoke", dest="revoke", action="store_true", help="Revoke a certificate.") parser.add_argument("-v", "--view-config-changes", @@ -102,7 +103,7 @@ def main(): sys.exit(1) # Use the same object if possible - if interfaces.IAuthenticator.providedBy(installer): + if interfaces.IAuthenticator.providedBy(installer): # pylint: disable=no-member auth = installer else: auth = determine_authenticator() @@ -181,9 +182,7 @@ def get_all_names(installer): def determine_authenticator(): """Returns a valid IAuthenticator.""" try: - if interfaces.IAuthenticator.implementedBy( - configurator.ApacheConfigurator): - return configurator.ApacheConfigurator() + return configurator.ApacheConfigurator() except errors.LetsEncryptNoInstallationError: logging.info("Unable to determine a way to authenticate the server") @@ -191,9 +190,7 @@ def determine_authenticator(): def determine_installer(): """Returns a valid installer if one exists.""" try: - if interfaces.IInstaller.implementedBy( - configurator.ApacheConfigurator): - return configurator.ApacheConfigurator() + return configurator.ApacheConfigurator() except errors.LetsEncryptNoInstallationError: logging.info("Unable to find a way to install the certificate.") diff --git a/setup.py b/setup.py index 6c49c2f2e..4f192b886 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ from setuptools import setup install_requires = [ 'argparse', 'jsonschema', - 'M2Crypto', 'mock', 'pycrypto', 'python-augeas', @@ -13,6 +12,9 @@ install_requires = [ 'requests', 'zope.component', 'zope.interface', + # order of items in install_requires DOES matter and M2Crypto has + # to go last, see #152 + 'M2Crypto', ] docs_extras = [ @@ -24,6 +26,7 @@ testing_extras = [ 'nose', 'nosexcover', 'pylint<1.4', # py2.6 compat, c.f #97 + 'astroid<1.3.0', # py2.6 compat, c.f. #187 'tox', ] diff --git a/tox.ini b/tox.ini index b2d7b9ae9..c8c671ca1 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ commands = [testenv:cover] commands = python setup.py dev - python setup.py nosetests --with-coverage --cover-min-percentage=59 + python setup.py nosetests --with-coverage --cover-min-percentage=61 [testenv:lint] commands =