diff --git a/.pylintrc b/.pylintrc index a1f7b7cb6..44fc15b1c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -322,11 +322,7 @@ max-attributes=7 min-public-methods=2 # Maximum number of public methods for a class (see R0904). -# Pylint counts all of the public methods that you also inherit. -# This has been reported/fixed as a bug, but until our version is fixed, -# I think this will only cause us headaches. (Unittests are automatically over) -# https://bitbucket.org/logilab/pylint/issue/248/too-many-public-methods-triggered-from -max-public-methods=100 +max-public-methods=20 [EXCEPTIONS] diff --git a/.travis.yml b/.travis.yml index 4ef4bb735..7f625f663 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,5 @@ -# To mimic README.md installation and hacking instructions as much as -# possible, this config file instructs Travis CI to create a build -# environment for each supported Python version, and then for each of -# those it runs tox with two environments: lint and pyXX corresponding -# to the currently used Travis CI build Python version. - language: python -python: - - "2.6" - - "2.7" - before_install: > travis_retry sudo apt-get install python python-setuptools python-virtualenv python-dev gcc swig dialog libaugeas0 libssl-dev @@ -18,7 +8,8 @@ install: travis_retry python setup.py dev # installs tox script: travis_retry tox env: - - TOXENV=py${TRAVIS_PYTHON_VERSION//[.]/} + - TOXENV=py26 + - TOXENV=py27 - TOXENV=lint - TOXENV=cover diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 000000000..741d9bc7c --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,27 @@ +ChangeLog +========= + +Please note: +the change log will only get updated after first release - for now please use the +`commit log `_. + + +Release 0.1.0 (not released yet) +-------------------------------- + +New Features: + +* ... + +Fixes: + +* ... + +Other changes: + +* ... + +Release 0.0.0 (not released yet) +-------------------------------- + +Initial release. diff --git a/MANIFEST.in b/MANIFEST.in index 24da8604e..0c082ea32 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ +include README.rst CHANGES.rst recursive-include letsencrypt *.json recursive-include letsencrypt *.sh recursive-include letsencrypt *.conf diff --git a/README.md b/README.md deleted file mode 100644 index 320943156..000000000 --- a/README.md +++ /dev/null @@ -1,143 +0,0 @@ -# Let's Encrypt - -[![Build Status] -(https://travis-ci.org/letsencrypt/lets-encrypt-preview.svg?branch=master)] -(https://travis-ci.org/letsencrypt/lets-encrypt-preview) - -## Disclaimer - -This is the [Let's Encrypt] Agent **DEVELOPER PREVIEW** repository. - -**DO NOT RUN THIS CODE ON A PRODUCTION WEBSERVER. IT WILL INSTALL -CERTIFICATES SIGNED BY A TEST CA, AND WILL CAUSE CERT WARNINGS FOR -USERS.** - -This code is intended for testing, demonstration, and integration -engineering with OSes and hosting platforms. For the time being -project focuses on Linux and Apache, though we will be expanding -it to other platforms. - -## Running the demo code - -The demo code is supported and known to work on **Ubuntu only** (even -closely related [Debian is known to fail] -(https://github.com/letsencrypt/lets-encrypt-preview/issues/68)). -Therefore, prerequisites for other platforms listed below are provided -mainly for the [developers](#hacking) reference. - -### Prerequisites - -In general: - -* [swig] is required for compiling [m2crypto] -* [augeas] is required for the `python-augeas` bindings - -#### Ubuntu - -``` -sudo apt-get install python python-setuptools python-virtualenv \ - python-dev gcc swig dialog libaugeas0 libssl-dev ca-certificates -``` - -#### Mac OSX - -`sudo brew install augeas swig` - -### Installation - -``` -virtualenv --no-site-packages -p python2 venv -./venv/bin/python setup.py install -sudo ./venv/bin/letsencrypt -``` - -## Hacking - -In order to start hacking, you will first have to create a development -environment: - -`./venv/bin/python setup.py dev` - -The code base, including your pull requests, **must have 100% test -statement coverage and be compliant with the [coding -style](#coding-style)**. The following tools are there to help you: - -- `./venv/bin/tox` starts a full set of tests. Please make sure you - run it before submitting a new pull request. - -- `./venv/bin/tox -e cover` checks the test coverage only. - -- `./venv/bin/tox -e lint` checks the style of the whole project, - while `./venv/bin/pylint --rcfile=.pylintrc file` will check a single `file` only. - -## Documentation - -The official documentation is available at -https://letsencrypt.readthedocs.org. - -In order to generate the Sphinx documentation, run the following -commands. - -``` -./venv/bin/python setup.py docs -cd docs -make clean html SPHINXBUILD=../venv/bin/sphinx-build -``` - -This should generate documentation in the `docs/_build/html` -directory. - -### Coding style - -Most importantly, **be consistent with the rest of the code**, please. - -1. Read [PEP 8 - Style Guide for Python Code] -(https://www.python.org/dev/peps/pep-0008). - -2. Follow [Google Python Style Guide] -(https://google-styleguide.googlecode.com/svn/trunk/pyguide.html), -with the exception that we use [Sphinx](http://sphinx-doc.org/)-style -documentation: - - ```python - def foo(arg): - """Short description. - - :param int arg: Some number. - - :returns: Argument - :rtype: int - - """ - return arg - ``` - -3. Remember to use `./venv/bin/pylint`. - -## Command line usage - -The letsencrypt commandline tool has a builtin help: - -``` -letsencrypt --help -``` - -## More Information - -- Further setup, documentation and open projects are available in the - [Wiki]. - -- Join us at our IRC channel: #letsencrypt at [Freenode]. - -- Client software development can be discussed on this [mailing - list]. To subscribe without a Google account, send an email to - client-dev+subscribe@letsencrypt.org. - - -[augeas]: http://augeas.net -[Freenode]: https://freenode.net -[Let's Encrypt]: https://letsencrypt.org -[m2crypto]: https://github.com/M2Crypto/M2Crypto -[mailing list]: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev -[swig]: http://www.swig.org -[wiki]: https://github.com/letsencrypt/lets-encrypt-preview/wiki diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..277dbe9de --- /dev/null +++ b/README.rst @@ -0,0 +1,79 @@ +About the Let's Encrypt Client +============================== + +In short: getting and installing SSL/TLS certificates made easy (`watch demo video`_). + +The Let's Encrypt Client is a tool to automatically receive and install +X.509 certificates to enable TLS on servers. The client will +interoperate with the Let's Encrypt CA which will be issuing browser-trusted +certificates for free beginning the summer of 2015. + +It's all automated: + +* The tool will prove domain control to the CA and submit a CSR (Certificate + Signing Request). +* If domain control has been proven, a certificate will get issued and the tool + will automatically install it. + +All you need to do is: + +:: + + user@www:~$ sudo letsencrypt www.example.org + + +**Encrypt ALL the things!** + + +.. image:: https://travis-ci.org/letsencrypt/lets-encrypt-preview.svg?branch=master + :target: https://travis-ci.org/letsencrypt/lets-encrypt-preview + +.. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU + + +Disclaimer +---------- + +This is a **DEVELOPER PREVIEW** intended for developers and testers only. + +**DO NOT RUN THIS CODE ON A PRODUCTION SERVER. IT WILL INSTALL CERTIFICATES +SIGNED BY A TEST CA, AND WILL CAUSE CERT WARNINGS FOR USERS.** + + +Current Features +---------------- + +* web servers supported: + + - apache2.x (tested and working on Ubuntu Linux) + +* the private key is generated locally on your system +* can talk to the Let's Encrypt (demo) CA or optionally to other ACME + compliant services +* can get domain-validated (DV) certificates +* can revoke certificates +* adjustable RSA key bitlength (2048 (default), 4096, ...) +* optionally can install a http->https redirect, so your site effectively + runs https only +* fully automated +* configuration changes are logged and can be reverted using the CLI +* text and ncurses UI +* Free and Open Source Software, made with Python. + + +Links +----- + +Documentation: https://letsencrypt.readthedocs.org/ + +Software project: https://github.com/letsencrypt/lets-encrypt-preview + +Main Website: https://letsencrypt.org/ + +IRC Channel: #letsencrypt on `Freenode`_ + +Mailing list: `client-dev`_ (to subscribe without a Google account, send an +email to client-dev+subscribe@letsencrypt.org) + +.. _Freenode: https://freenode.net +.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 000000000..8668ec5d8 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,8 @@ +================= +API Documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/docs/api/client/interactive_challenge.rst b/docs/api/client/interactive_challenge.rst deleted file mode 100644 index 38f14f115..000000000 --- a/docs/api/client/interactive_challenge.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.client.interactive_challenge` ------------------------------------------------ - -.. automodule:: letsencrypt.client.interactive_challenge - :members: diff --git a/docs/api/client/recovery_contact_challenge.rst b/docs/api/client/recovery_contact_challenge.rst deleted file mode 100644 index 3b6e12a0f..000000000 --- a/docs/api/client/recovery_contact_challenge.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.client.recovery_contact_challenge` ----------------------------------------------------- - -.. automodule:: letsencrypt.client.recovery_contact_challenge - :members: diff --git a/docs/conf.py b/docs/conf.py index fd089d14b..018d2afed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,13 +12,22 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys +import codecs import os +import re +import sys + +here = os.path.abspath(os.path.dirname(__file__)) + +# read version number (and other metadata) from package init +init_fn = os.path.join(here, '..', 'letsencrypt', '__init__.py') +with codecs.open(init_fn, encoding='utf8') as fd: + meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", fd.read())) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) # -- General configuration ------------------------------------------------ @@ -34,6 +43,7 @@ extensions = [ 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', + 'repoze.sphinx.autointerface', ] # Add any paths that contain templates here, relative to this directory. @@ -57,9 +67,9 @@ copyright = u'2014, Let\'s Encrypt Project' # built documents. # # The short X.Y version. -version = '0.1' +version = '.'.join(meta['version'].split('.')[:2]) # The full version, including alpha/beta/rc tags. -release = '0.1' +release = meta['version'] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index c636507df..b290b2231 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,18 +1,17 @@ -.. Let's Encrypt documentation master file, created by - sphinx-quickstart on Sun Nov 23 20:35:21 2014. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Let's Encrypt's documentation! -========================================= - -API documentation ------------------ +Welcome to the Let's Encrypt client documentation! +================================================== .. toctree:: - :glob: + :maxdepth: 2 - api/** + intro + using + project + +.. toctree:: + :maxdepth: 1 + + api Indices and tables @@ -21,4 +20,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/docs/intro.rst b/docs/intro.rst new file mode 100644 index 000000000..188ff4302 --- /dev/null +++ b/docs/intro.rst @@ -0,0 +1,6 @@ +============ +Introduction +============ + +.. include:: ../README.rst +.. include:: ../CHANGES.rst diff --git a/docs/project.rst b/docs/project.rst new file mode 100644 index 000000000..fa59c1af3 --- /dev/null +++ b/docs/project.rst @@ -0,0 +1,77 @@ +================================ +The Let's Encrypt Client Project +================================ + +.. _hacking: + +Hacking +======= + +In order to start hacking, you will first have to create a development +environment: + +:: + + ./venv/bin/python setup.py dev + +The code base, including your pull requests, **must** have 100% test statement +coverage **and** be compliant with the :ref:`coding-style`. + +The following tools are there to help you: + +- ``./venv/bin/tox`` starts a full set of tests. Please make sure you + run it before submitting a new pull request. + +- ``./venv/bin/tox -e cover`` checks the test coverage only. + +- ``./venv/bin/tox -e lint`` checks the style of the whole project, + while ``./venv/bin/pylint --rcfile=.pylintrc file`` will check a single `file` only. + + +.. _coding-style: + +Coding style +============ + +Please: + +1. **Be consistent with the rest of the code**. + +2. Read `PEP 8 - Style Guide for Python Code`_. + +3. Follow the `Google Python Style Guide`_, with the exception that we + use `Sphinx-style`_ documentation: + + :: + + def foo(arg): + """Short description. + + :param int arg: Some number. + + :returns: Argument + :rtype: int + + """ + return arg + +4. Remember to use ``./venv/bin/pylint``. + +.. _Google Python Style Guide: https://google-styleguide.googlecode.com/svn/trunk/pyguide.html +.. _Sphinx-style: http://sphinx-doc.org/ +.. _PEP 8 - Style Guide for Python Code: https://www.python.org/dev/peps/pep-0008 + + +Updating the Documentation +========================== + +In order to generate the Sphinx documentation, run the following commands. + +:: + + ./venv/bin/python setup.py docs + cd docs + make clean html SPHINXBUILD=../venv/bin/sphinx-build + + +This should generate documentation in the ``docs/_build/html`` directory. diff --git a/docs/using.rst b/docs/using.rst new file mode 100644 index 000000000..441bf1623 --- /dev/null +++ b/docs/using.rst @@ -0,0 +1,60 @@ +============================== +Using the Let's Encrypt client +============================== + +Prerequisites +============= + +The demo code is supported and known to work on **Ubuntu only** (even +closely related `Debian is known to fail`_). + +Therefore, prerequisites for other platforms listed below are provided +mainly for the :ref:`developers ` reference. + +In general: + +* `swig`_ is required for compiling `m2crypto`_ +* `augeas`_ is required for the ``python-augeas`` bindings + +.. _Debian is known to fail: https://github.com/letsencrypt/lets-encrypt-preview/issues/68 + +Ubuntu +------ + +:: + + sudo apt-get install python python-setuptools python-virtualenv python-dev \ + gcc swig dialog libaugeas0 libssl-dev ca-certificates + + +Mac OSX +------- + +:: + + sudo brew install augeas swig + + +Installation +============ + +:: + + virtualenv --no-site-packages -p python2 venv + ./venv/bin/python setup.py install + sudo ./venv/bin/letsencrypt + + +Usage +===== + +The letsencrypt commandline tool has a builtin help: + +:: + + letsencrypt --help + + +.. _augeas: http://augeas.net/ +.. _m2crypto: https://github.com/M2Crypto/M2Crypto +.. _swig: http://www.swig.org/ diff --git a/letsencrypt/__init__.py b/letsencrypt/__init__.py index 9c0ff7662..b36747b5f 100644 --- a/letsencrypt/__init__.py +++ b/letsencrypt/__init__.py @@ -1 +1,3 @@ """Let's Encrypt.""" +# version number like 1.2.3a0, must have at least 2 parts, like 1.2 +__version__ = "0.1" diff --git a/letsencrypt/client/apache/configurator.py b/letsencrypt/client/apache/configurator.py index b4d07985a..ad6e54273 100644 --- a/letsencrypt/client/apache/configurator.py +++ b/letsencrypt/client/apache/configurator.py @@ -43,7 +43,7 @@ from letsencrypt.client.apache import parser class ApacheConfigurator(augeas_configurator.AugeasConfigurator): - # pylint: disable=too-many-instance-attributes + # pylint: disable=too-many-instance-attributes,too-many-public-methods """Apache configurator. State of Configurator: This code has been tested under Ubuntu 12.04 diff --git a/letsencrypt/client/apache/dvsni.py b/letsencrypt/client/apache/dvsni.py index 668cdc76c..feb25c3eb 100644 --- a/letsencrypt/client/apache/dvsni.py +++ b/letsencrypt/client/apache/dvsni.py @@ -87,11 +87,7 @@ class ApacheDvsni(object): # Create all of the challenge certs for chall in self.dvsni_chall: - cert_path = self.get_cert_file(chall.nonce) - self.config.reverter.register_file_creation(True, cert_path) - s_b64 = challenge_util.dvsni_gen_cert( - cert_path, chall.domain, chall.r_b64, chall.nonce, chall.key) - + s_b64 = self._setup_challenge_cert(chall) responses.append({"type": "dvsni", "s": s_b64}) # Setup the configuration @@ -102,6 +98,21 @@ class ApacheDvsni(object): return responses + def _setup_challenge_cert(self, chall): + """Generate and write out challenge certificate.""" + cert_path = self.get_cert_file(chall.nonce) + # Register the path before you write out the file + self.config.reverter.register_file_creation(True, cert_path) + + cert_pem, s_b64 = challenge_util.dvsni_gen_cert( + chall.domain, chall.r_b64, chall.nonce, chall.key) + + # Write out challenge cert + with open(cert_path, 'w') as cert_chall_fd: + cert_chall_fd.write(cert_pem) + + return s_b64 + def _mod_config(self, ll_addrs): """Modifies Apache config files to include challenge vhosts. diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index c2d94ecec..e2ff9d292 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -123,7 +123,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes self._cleanup_challenges(domain) def _satisfy_challenges(self): - """Attempt to satisfy all saved challenge messages.""" + """Attempt to satisfy all saved challenge messages. + + .. todo:: It might be worth it to try different challenges to + find one that doesn't throw an exception + + """ logging.info("Performing the following challenges:") for dom in self.domains: self.paths[dom] = gen_challenge_path( @@ -143,8 +148,19 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes flat_client.extend(ichall.chall for ichall in self.client_c[dom]) flat_auth.extend(ichall.chall for ichall in self.dv_c[dom]) - client_resp = self.client_auth.perform(flat_client) - dv_resp = self.dv_auth.perform(flat_auth) + try: + client_resp = self.client_auth.perform(flat_client) + dv_resp = self.dv_auth.perform(flat_auth) + # This will catch both specific types of errors. + except errors.LetsEncryptAuthHandlerError as err: + logging.critical("Failure in setting up challenges:") + logging.critical(str(err)) + logging.info("Attempting to clean up outstanding challenges...") + for dom in self.domains: + self._cleanup_challenges(dom) + + raise errors.LetsEncryptAuthHandlerError( + "Unable to perform challenges") logging.info("Ready for verification...") @@ -191,8 +207,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes """ logging.info("Cleaning up challenges for %s", domain) - self.dv_auth.cleanup(self.dv_c[domain]) - self.client_auth.cleanup(self.client_c[domain]) + # These are indexed challenges... give just the challenges to the auth + # Chose to make these lists instead of a generator to make it easier to + # work with... + self.dv_auth.cleanup([ichall.chall for ichall in self.dv_c[domain]]) + self.client_auth.cleanup( + [ichall.chall for ichall in self.client_c[domain]]) def _cleanup_state(self, delete_list): """Cleanup state after an authorization is received. @@ -279,8 +299,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes elif chall["type"] == "dns": logging.info(" DNS challenge for name %s.", domain) - return challenge_util.DnsChall( - domain, str(chall["token"]), self.authkey[domain]) + return challenge_util.DnsChall(domain, str(chall["token"])) else: raise errors.LetsEncryptClientError( diff --git a/letsencrypt/client/challenge_util.py b/letsencrypt/client/challenge_util.py index b30eb651f..d1b105d8f 100644 --- a/letsencrypt/client/challenge_util.py +++ b/letsencrypt/client/challenge_util.py @@ -13,7 +13,7 @@ from letsencrypt.client import le_util DvsniChall = collections.namedtuple("DvsniChall", "domain, r_b64, nonce, key") SimpleHttpsChall = collections.namedtuple( "SimpleHttpsChall", "domain, token, key") -DnsChall = collections.namedtuple("DnsChall", "domain, token, key") +DnsChall = collections.namedtuple("DnsChall", "domain, token") # Client Challenges RecContactChall = collections.namedtuple( @@ -27,11 +27,9 @@ IndexedChall = collections.namedtuple("IndexedChall", "chall, index") # DVSNI Challenge functions -def dvsni_gen_cert(filepath, name, r_b64, nonce, key): +def dvsni_gen_cert(name, r_b64, nonce, key): """Generate a DVSNI cert and save it to filepath. - :param str filepath: destination to save certificate. This will overwrite - any file that is currently at the location. :param str name: domain to validate :param str r_b64: jose base64 encoded dvsni r value :param str nonce: hex value of nonce @@ -39,8 +37,10 @@ def dvsni_gen_cert(filepath, name, r_b64, nonce, key): :param key: Key to perform challenge :type key: :class:`letsencrypt.client.le_util.Key` - :returns: dvsni s value jose base64 encoded - :rtype: str + :returns: tuple of (cert_pem, s) where + cert_pem is the certificate in pem form + s is the dvsni s value, jose base64 encoded + :rtype: tuple """ # Generate S @@ -53,10 +53,7 @@ def dvsni_gen_cert(filepath, name, r_b64, nonce, key): cert_pem = crypto_util.make_ss_cert( key.pem, [nonce + CONFIG.INVALID_EXT, name, ext]) - with open(filepath, "w") as chall_cert_file: - chall_cert_file.write(cert_pem) - - return le_util.jose_b64encode(dvsni_s) + return cert_pem, le_util.jose_b64encode(dvsni_s) def _dvsni_gen_ext(dvsni_r, dvsni_s): diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 37dcc2ead..9f70bc19e 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -3,8 +3,6 @@ import csv import logging import os import shutil -import socket -import string import sys import M2Crypto @@ -24,11 +22,6 @@ from letsencrypt.client import revoker from letsencrypt.client.apache import configurator -# it's weird to point to ACME servers via raw IPv6 addresses, and -# such addresses can be %SCARY in some contexts, so out of paranoia -# let's disable them by default -ALLOW_RAW_IPV6_SERVER = False - class Client(object): """ACME protocol client. @@ -91,8 +84,6 @@ class Client(object): logging.warning("Unable to obtain a certificate, because client " "does not have a valid auth handler.") - sanity_check_names(domains) - # Request Challenges for name in domains: self.auth_handler.add_chall_msg( @@ -395,47 +386,6 @@ def csr_pem_to_der(csr): return le_util.CSR(csr.file, csr_obj.as_der(), "der") -def sanity_check_names(names): - """Make sure host names are valid. - - :param list names: List of host names - - """ - for name in names: - if not is_hostname_sane(name): - logging.fatal("%r is an impossible hostname", name) - sys.exit(81) - - -def is_hostname_sane(hostname): - """Make sure the given host name is sane. - - Do enough to avoid shellcode from the environment. There's - no need to do more. - - :param str hostname: Host name to validate - - :returns: True if hostname is valid, otherwise false. - :rtype: bool - - """ - # hostnames & IPv4 - allowed = string.ascii_letters + string.digits + "-." - if all([c in allowed for c in hostname]): - return True - - if not ALLOW_RAW_IPV6_SERVER: - return False - - # ipv6 is messy and complicated, can contain %zoneindex etc. - try: - # is this a valid IPv6 address? - socket.getaddrinfo(hostname, 443, socket.AF_INET6) - return True - except socket.error: - return False - - # This should be controlled by commandline parameters def determine_authenticator(): """Returns a valid IAuthenticator.""" diff --git a/letsencrypt/client/errors.py b/letsencrypt/client/errors.py index 6a3739832..d49611ce7 100644 --- a/letsencrypt/client/errors.py +++ b/letsencrypt/client/errors.py @@ -9,6 +9,7 @@ class LetsEncryptReverterError(LetsEncryptClientError): """Let's Encrypt Reverter error.""" +# Auth Handler Errors class LetsEncryptAuthHandlerError(LetsEncryptClientError): """Let's Encrypt Auth Handler error.""" @@ -17,6 +18,16 @@ class LetsEncryptClientAuthError(LetsEncryptAuthHandlerError): """Let's Encrypt Client Authenticator error.""" +class LetsEncryptDvAuthError(LetsEncryptAuthHandlerError): + """Let's Encrypt DV Authenticator error.""" + + +# Authenticator - Challenge specific errors +class LetsEncryptDvsniError(LetsEncryptDvAuthError): + """Let's Encrypt DVSNI error.""" + + +# Configurator Errors class LetsEncryptConfiguratorError(LetsEncryptClientError): """Let's Encrypt Configurator error.""" @@ -28,6 +39,3 @@ class LetsEncryptNoInstallationError(LetsEncryptConfiguratorError): class LetsEncryptMisconfigurationError(LetsEncryptConfiguratorError): """Let's Encrypt Misconfiguration error.""" - -class LetsEncryptDvsniError(LetsEncryptConfiguratorError): - """Let's Encrypt DVSNI error.""" diff --git a/letsencrypt/client/interactive_challenge.py b/letsencrypt/client/interactive_challenge.py deleted file mode 100644 index 4130525f5..000000000 --- a/letsencrypt/client/interactive_challenge.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Interactive challenge.""" -import textwrap - -import dialog -import zope.interface - -from letsencrypt.client import interfaces - - -class InteractiveChallenge(object): - """Interactive challenge. - - Interactive challenge displays the string sent by the CA formatted - to fit on the screen of the client. The Challenge also adds proper - instructions for how the client should continue the letsencrypt - process. - - """ - zope.interface.implements(interfaces.IChallenge) - - BOX_SIZE = 70 - - def __init__(self, string): - super(InteractiveChallenge, self).__init__() - self.string = string - - def perform(self, quiet=True): # pylint: disable=missing-docstring - if quiet: - dialog.Dialog().msgbox( - self.get_display_string(), width=self.BOX_SIZE) - else: - print self.get_display_string() - raw_input('') - - return True - - def get_display_string(self): # pylint: disable=missing-docstring - return (textwrap.fill(self.string, width=self.BOX_SIZE) + - "\n\nPlease Press Enter to Continue") - - # def formatted_reasons(self): - # return "\n\t* %s\n", self.reason diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index 9e35a754a..1c6d4766f 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -1,7 +1,7 @@ """Let's Encrypt client interfaces.""" import zope.interface -# pylint: disable=no-self-argument,no-method-argument,no-init +# pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class class IAuthenticator(zope.interface.Interface): @@ -11,6 +11,7 @@ class IAuthenticator(zope.interface.Interface): ability to perform challenges and attain a certificate. """ + def get_chall_pref(domain): """Return list of challenge preferences. @@ -22,19 +23,24 @@ class IAuthenticator(zope.interface.Interface): :rtype: list """ + def perform(chall_list): """Perform the given challenge. :param list chall_list: List of namedtuple types defined in - challenge_util.py. DvsniChall...ect.. + :mod:`letsencrypt.client.challenge_util` (``DvsniChall``, etc.). - :returns: List of responses - If the challenge cant be completed... - None - Authenticator can perform challenge, but can't at this time - False - Authenticator will never be able to perform (error) - :rtype: `list` of dicts + :returns: Challenge responses or if it cannot be completed then: + + ``None`` + Authenticator can perform challenge, but can't at this time + ``False`` + Authenticator will never be able to perform (error) + + :rtype: :class:`list` of :class:`dict` """ + def cleanup(chall_list): """Revert changes and shutdown after challenges complete.""" @@ -58,6 +64,7 @@ class IInstaller(zope.interface.Interface): Represents any server that an X509 certificate can be placed. """ + def get_all_names(): """Returns all names that may be authenticated.""" @@ -69,35 +76,42 @@ class IInstaller(zope.interface.Interface): :param str key: private key filename """ - def enhance(domain, enhancment, options=None): - """Peform a configuration enhancment. + + def enhance(domain, enhancement, options=None): + """Perform a configuration enhancement. :param str domain: domain for which to provide enhancement - :param str enhancement: An enhancement as defined in CONFIG.ENHANCEMENTS - :param options: flexible options parameter for enhancement - :type options: Check documentation of - :class:`letsencrypt.client.CONFIG.ENHANCEMENTS` for expected options - for each enhancement. + :param str enhancement: An enhancement as defined in + :const:`~letsencrypt.client.CONFIG.ENHANCEMENTS` + :param options: Flexible options parameter for enhancement. + Check documentation of + :const:`~letsencrypt.client.CONFIG.ENHANCEMENTS` + for expected options for each enhancement. """ + def supported_enhancements(): - """Returns a list of supported enhancments. + """Returns a list of supported enhancements. - :returns: supported enhancments which should be a subset of the - enhancments in :class:`letsencrypt.client.CONFIG.ENHANCEMENTS` - :rtype: `list` of `str` + :returns: supported enhancements which should be a subset of + :const:`~letsencrypt.client.CONFIG.ENHANCEMENTS` + :rtype: :class:`list` of :class:`str` """ + def get_all_certs_keys(): """Retrieve all certs and keys set in configuration. - :returns: list of tuples with form [(cert, key, path)] - cert - str path to certificate file - key - str path to associated key file - path - file path to configuration file + :returns: tuples with form `[(cert, key, path)]`, where: + + - `cert` - str path to certificate file + - `key` - str path to associated key file + - `path` - file path to configuration file + :rtype: list """ + def save(title=None, temporary=False): """Saves all changes to the configuration files. @@ -113,6 +127,7 @@ class IInstaller(zope.interface.Interface): be quickly reversed in the future (challenges) """ + def rollback_checkpoints(rollback=1): """Revert `rollback` number of configuration checkpoints.""" @@ -135,14 +150,19 @@ class IDisplay(zope.interface.Interface): :param str message: Message to display """ + def generic_menu(message, choices, input_text=""): """Displays a generic menu. :param str message: message to display - :param tup choices: choices formated as a `list` of `tup` + + :param choices: choices + :type choices: :class:`list` of :func:`tuple` + :param str input_text: instructions on how to make a selection """ + def generic_input(message): """Accept input from the user.""" @@ -168,7 +188,7 @@ class IDisplay(zope.interface.Interface): """Ask the user whether they would like to redirect to HTTPS.""" -class IValidator(object): +class IValidator(zope.interface.Interface): """Configuration validator.""" def redirect(name): @@ -178,7 +198,7 @@ class IValidator(object): """Verify ocsp stapling for domain.""" def https(names): - """Verifiy HTTPS is enabled for domain.""" + """Verify HTTPS is enabled for domain.""" def hsts(name): """Verify HSTS header is enabled.""" diff --git a/letsencrypt/client/log.py b/letsencrypt/client/log.py index 19d33c53a..91319156b 100644 --- a/letsencrypt/client/log.py +++ b/letsencrypt/client/log.py @@ -6,7 +6,7 @@ import dialog from letsencrypt.client import display -class DialogHandler(logging.Handler): +class DialogHandler(logging.Handler): # pylint: disable=too-few-public-methods """Logging handler using dialog info box. :ivar int height: Height of the info box (without padding). diff --git a/letsencrypt/client/recovery_contact_challenge.py b/letsencrypt/client/recovery_contact_challenge.py deleted file mode 100644 index 6cfab00d0..000000000 --- a/letsencrypt/client/recovery_contact_challenge.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Recovery Contact Identifier Validation Challenge. - -.. note:: This class is not complete and is not included in the project - currently. - -""" -import time - -import dialog -import requests -import zope.interface - -from letsencrypt.client import interfaces - - -class RecoveryContact(object): - """Recovery Contact Identifier Validation Challenge. - - Based on draft-barnes-acme, section 6.3. - - """ - zope.interface.implements(interfaces.IChallenge) - - def __init__(self, activation_url="", success_url="", contact="", - poll_delay=3): - super(RecoveryContact, self).__init__() - self.token = "" - self.activation_url = activation_url - self.success_url = success_url - self.contact = contact - self.poll_delay = poll_delay - - 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()) - return self.poll(10, quiet) - else: - code, self.token = d.inputbox(self.get_display_string()) - if code != d.OK: - return False - - else: - print self.get_display_string() - if self.success_url: - return self.poll(10, quiet) - else: - self.token = raw_input("Enter the recovery token:") - - return True - - def cleanup(self): # pylint: disable=no-self-use,missing-docstring - return - - def get_display_string(self): - """Create information message for the user. - - :returns: Message to be displayed to the user. - :rtype: str - - """ - msg = "Recovery Contact Challenge: " - if self.activation_url: - msg += "Proceed to the URL to continue " + self.activation_url - - if self.activation_url and self.contact: - msg += " or respond to the recovery email sent to " + self.contact - elif self.contact: - msg += "Recovery email sent to" + self.contact - - return msg - - def poll(self, rounds=10, quiet=True): - """Poll the server. - - :param int rounds: Number of poll attempts. - :param bool quiet: Display dialog box if True, raw prompt otherwise. - - :returns: - :rtype: bool - - """ - for _ in xrange(rounds): - if requests.get(self.success_url).status_code != 200: - time.sleep(self.poll_delay) - else: - return True - if self.prompt_continue(quiet): - return self.poll(rounds, quiet) - else: - return False - - 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. - - :returns: True if user agreed, False otherwise. - :rtype: bool - - """ - prompt = ("You have not completed the challenge yet, " - "would you like to continue?") - if quiet: - ans = dialog.Dialog().yesno(prompt, width=70) - else: - ans = raw_input(prompt + "y/n") - - return ans.startswith('y') or ans.startswith('Y') - - def generate_response(self): # pylint: disable=missing-docstring - if not self.token: - return {"type": "recoveryContact"} - return { - "type": "recoveryContact", - "token": self.token, - } diff --git a/letsencrypt/client/reverter.py b/letsencrypt/client/reverter.py index 8342ecd06..4bb2bd46c 100644 --- a/letsencrypt/client/reverter.py +++ b/letsencrypt/client/reverter.py @@ -4,8 +4,12 @@ import os import shutil import time +import zope.component + from letsencrypt.client import CONFIG +from letsencrypt.client import display from letsencrypt.client import errors +from letsencrypt.client import interfaces from letsencrypt.client import le_util @@ -44,6 +48,10 @@ class Reverter(object): :param int rollback: Number of checkpoints to reverse. A str num will be cast to an integer. So '2' is also acceptable. + :raises :class:`letsencrypt.client.errors.LetsEncryptReverterError`: If + there is a problem with the input or if the function is unable to + correctly revert the configuration checkpoints. + """ try: rollback = int(rollback) @@ -96,26 +104,30 @@ class Reverter(object): raise errors.LetsEncryptReverterError( "Invalid directories in {0}".format(self.direc['backup'])) + output = [] for bkup in backups: - print time.ctime(float(bkup)) + output.append(time.ctime(float(bkup))) cur_dir = os.path.join(self.direc['backup'], bkup) with open(os.path.join(cur_dir, "CHANGES_SINCE")) as changes_fd: - print changes_fd.read() + output.append(changes_fd.read()) - print "Affected files:" + output.append("Affected files:") with open(os.path.join(cur_dir, "FILEPATHS")) as paths_fd: filepaths = paths_fd.read().splitlines() for path in filepaths: - print " {0}".format(path) + output.append(" {0}".format(path)) if os.path.isfile(os.path.join(cur_dir, "NEW_FILES")): with open(os.path.join(cur_dir, "NEW_FILES")) as new_fd: - print "New Configuration Files:" + output.append("New Configuration Files:") filepaths = new_fd.read().splitlines() for path in filepaths: - print " {0}".format(path) + output.append(" {0}".format(path)) - print "{0}".format(os.linesep) + output.append(os.linesep) + + zope.component.getUtility(interfaces.IDisplay).generic_notification( + os.linesep.join(output), display.HEIGHT) def add_to_temp_checkpoint(self, save_files, save_notes): """Add files to temporary checkpoint diff --git a/letsencrypt/client/setup.sh b/letsencrypt/client/setup.sh deleted file mode 100755 index 9db81cbd2..000000000 --- a/letsencrypt/client/setup.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -cp options-ssl.conf /etc/letsencrypt/options-ssl.conf diff --git a/letsencrypt/client/tests/acme_test.py b/letsencrypt/client/tests/acme_test.py index f3cf4a69a..514c6b14e 100644 --- a/letsencrypt/client/tests/acme_test.py +++ b/letsencrypt/client/tests/acme_test.py @@ -42,7 +42,7 @@ class ACMEObjectValidateTest(unittest.TestCase): self._test_fails('{"type": "foo", "price": "asd"}') -class PrettyTest(unittest.TestCase): +class PrettyTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Tests for letsencrypt.client.acme.pretty.""" @classmethod diff --git a/letsencrypt/client/tests/apache/dvsni_test.py b/letsencrypt/client/tests/apache/dvsni_test.py index 8f6593113..eedc32f87 100644 --- a/letsencrypt/client/tests/apache/dvsni_test.py +++ b/letsencrypt/client/tests/apache/dvsni_test.py @@ -56,23 +56,49 @@ class DvsniPerformTest(util.ApacheTest): self.assertTrue(resp is None) @mock.patch("letsencrypt.client.challenge_util.dvsni_gen_cert") - def test_perform1(self, mock_dvsni_gen_cert): + def test_setup_challenge_cert(self, mock_dvsni_gen_cert): + # This is a helper function that can be used for handling + # open context managers more elegantly. It avoids dealing with + # __enter__ and __exit__ calls. + # http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open chall = self.challs[0] - self.sni.add_chall(chall) - mock_dvsni_gen_cert.return_value = "randomS1" - responses = self.sni.perform() + m_open = mock.mock_open() + mock_dvsni_gen_cert.return_value = ("pem", "randomS1") + + with mock.patch("letsencrypt.client.apache.dvsni.open", + m_open, create=True): + # pylint: disable=protected-access + s_b64 = self.sni._setup_challenge_cert(chall) + + self.assertEqual(s_b64, "randomS1") + + self.assertTrue(m_open.called) + self.assertEqual( + m_open.call_args[0], (self.sni.get_cert_file(chall.nonce), 'w')) + self.assertEqual(m_open().write.call_args[0][0], "pem") self.assertEqual(mock_dvsni_gen_cert.call_count, 1) calls = mock_dvsni_gen_cert.call_args_list expected_call_list = [ - (self.sni.get_cert_file(chall.nonce), chall.domain, - chall.r_b64, chall.nonce, chall.key) + (chall.domain, chall.r_b64, chall.nonce, chall.key) ] - for i in range(len(expected_call_list)): - for j in range(len(expected_call_list[0])): + for i in xrange(len(expected_call_list)): + for j in xrange(len(expected_call_list[0])): self.assertEqual(calls[i][0][j], expected_call_list[i][j]) + def test_perform1(self): + chall = self.challs[0] + self.sni.add_chall(chall) + mock_setup_cert = mock.MagicMock(return_value="randomS1") + # pylint: disable=protected-access + self.sni._setup_challenge_cert = mock_setup_cert + + responses = self.sni.perform() + + mock_setup_cert.assert_called_once_with(chall) + + # Check to make sure challenge config path is included in apache config. self.assertEqual( len(self.sni.config.parser.find_dir( "Include", self.sni.challenge_conf)), @@ -80,33 +106,30 @@ class DvsniPerformTest(util.ApacheTest): self.assertEqual(len(responses), 1) self.assertEqual(responses[0]["s"], "randomS1") - @mock.patch("letsencrypt.client.challenge_util.dvsni_gen_cert") - def test_perform2(self, mock_dvsni_gen_cert): + def test_perform2(self): for chall in self.challs: self.sni.add_chall(chall) - mock_dvsni_gen_cert.side_effect = ["randomS0", "randomS1"] + mock_setup_cert = mock.MagicMock(side_effect=["randomS0", "randomS1"]) + # pylint: disable=protected-access + self.sni._setup_challenge_cert = mock_setup_cert + responses = self.sni.perform() - self.assertEqual(mock_dvsni_gen_cert.call_count, 2) - calls = mock_dvsni_gen_cert.call_args_list - expected_call_list = [] + self.assertEqual(mock_setup_cert.call_count, 2) - for chall in self.challs: - expected_call_list.append( - (self.sni.get_cert_file(chall.nonce), chall.domain, - chall.r_b64, chall.nonce, chall.key)) - - for i in range(len(expected_call_list)): - for j in range(len(expected_call_list[0])): - self.assertEqual(calls[i][0][j], expected_call_list[i][j]) + # Make sure calls made to mocked function were correct + self.assertEqual( + mock_setup_cert.call_args_list[0], mock.call(self.challs[0])) + self.assertEqual( + mock_setup_cert.call_args_list[1], mock.call(self.challs[1])) self.assertEqual( len(self.sni.config.parser.find_dir( "Include", self.sni.challenge_conf)), 1) self.assertEqual(len(responses), 2) - for i in range(2): + for i in xrange(2): self.assertEqual(responses[i]["s"], "randomS%d" % i) def test_mod_config(self): diff --git a/letsencrypt/client/tests/apache/util.py b/letsencrypt/client/tests/apache/util.py index d5a662924..fe27921b7 100644 --- a/letsencrypt/client/tests/apache/util.py +++ b/letsencrypt/client/tests/apache/util.py @@ -12,7 +12,7 @@ from letsencrypt.client.apache import configurator from letsencrypt.client.apache import obj -class ApacheTest(unittest.TestCase): +class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods def setUp(self): super(ApacheTest, self).setUp() diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index b80c3c61d..9c3effe1b 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -1,8 +1,10 @@ """Tests for letsencrypt.client.auth_handler.""" +import logging import unittest import mock +from letsencrypt.client import challenge_util from letsencrypt.client import errors from letsencrypt.client.tests import acme_util @@ -35,6 +37,11 @@ class SatisfyChallengesTest(unittest.TestCase): self.handler = AuthHandler( self.mock_dv_auth, self.mock_client_auth, None) + logging.disable(logging.CRITICAL) + + def tearDown(self): + logging.disable(logging.NOTSET) + def test_name1_dvsni1(self): dom = "0" challenge = [acme_util.CHALLENGES["dvsni"]] @@ -54,7 +61,7 @@ class SatisfyChallengesTest(unittest.TestCase): def test_name5_dvsni5(self): challenge = [acme_util.CHALLENGES["dvsni"]] - for i in range(5): + for i in xrange(5): self.handler.add_chall_msg( str(i), acme_util.get_chall_msg(str(i), "nonce%d" % i, challenge), @@ -67,14 +74,14 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.client_c), 5) # Each message contains 1 auth, 0 client - for i in range(5): + for i in xrange(5): dom = str(i) self.assertEqual(len(self.handler.responses[dom]), 1) self.assertEqual(self.handler.responses[dom][0], "DvsniChall%d" % i) self.assertEqual(len(self.handler.dv_c[dom]), 1) self.assertEqual(len(self.handler.client_c[dom]), 0) - self.assertEqual( - type(self.handler.dv_c[dom][0].chall).__name__, "DvsniChall") + self.assertTrue(isinstance(self.handler.dv_c[dom][0].chall, + challenge_util.DvsniChall)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") def test_name1_auth(self, mock_chall_path): @@ -102,8 +109,8 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.dv_c[dom]), 1) self.assertEqual(len(self.handler.client_c[dom]), 0) - self.assertEqual( - type(self.handler.dv_c[dom][0].chall).__name__, "SimpleHttpsChall") + self.assertTrue(isinstance(self.handler.dv_c[dom][0].chall, + challenge_util.SimpleHttpsChall)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") def test_name1_all(self, mock_chall_path): @@ -131,16 +138,16 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual( self.handler.responses[dom], self._get_exp_response(dom, path, challenges)) - self.assertEqual( - type(self.handler.dv_c[dom][0].chall).__name__, "SimpleHttpsChall") - self.assertEqual( - type(self.handler.client_c[dom][0].chall).__name__, "RecTokenChall") + self.assertTrue(isinstance(self.handler.dv_c[dom][0].chall, + challenge_util.SimpleHttpsChall)) + self.assertTrue(isinstance(self.handler.client_c[dom][0].chall, + challenge_util.RecTokenChall)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") def test_name5_all(self, mock_chall_path): challenges = acme_util.get_challenges() combos = acme_util.gen_combos(challenges) - for i in range(5): + for i in xrange(5): self.handler.add_chall_msg( str(i), acme_util.get_chall_msg( @@ -153,7 +160,7 @@ class SatisfyChallengesTest(unittest.TestCase): self.handler._satisfy_challenges() # pylint: disable=protected-access self.assertEqual(len(self.handler.responses), 5) - for i in range(5): + for i in xrange(5): self.assertEqual( len(self.handler.responses[str(i)]), len(challenges)) self.assertEqual(len(self.handler.dv_c), 5) @@ -167,11 +174,10 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.dv_c[dom]), 1) self.assertEqual(len(self.handler.client_c[dom]), 1) - self.assertEqual( - type(self.handler.dv_c[dom][0].chall).__name__, "DvsniChall") - self.assertEqual( - type(self.handler.client_c[dom][0].chall).__name__, - "RecContactChall") + self.assertTrue(isinstance(self.handler.dv_c[dom][0].chall, + challenge_util.DvsniChall)) + self.assertTrue(isinstance(self.handler.client_c[dom][0].chall, + challenge_util.RecContactChall)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") def test_name5_mix(self, mock_chall_path): @@ -188,7 +194,7 @@ class SatisfyChallengesTest(unittest.TestCase): acme_util.get_challenges()] # Combos doesn't matter since I am overriding the gen_path function - for i in range(5): + for i in xrange(5): dom = str(i) paths.append(gen_path(chosen_chall[i], challenge_list[i])) self.handler.add_chall_msg( @@ -205,7 +211,7 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.dv_c), 5) self.assertEqual(len(self.handler.client_c), 5) - for i in range(5): + for i in xrange(5): dom = str(i) resp = self._get_exp_response(i, paths[i], challenge_list[i]) self.assertEqual(self.handler.responses[dom], resp) @@ -213,21 +219,66 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual( len(self.handler.client_c[dom]), len(chosen_chall[i]) - 1) - self.assertEqual( - type(self.handler.dv_c["0"][0].chall).__name__, "DnsChall") - self.assertEqual( - type(self.handler.dv_c["1"][0].chall).__name__, "DvsniChall") - self.assertEqual( - type(self.handler.dv_c["2"][0].chall).__name__, "SimpleHttpsChall") - self.assertEqual( - type(self.handler.dv_c["3"][0].chall).__name__, "SimpleHttpsChall") - self.assertEqual( - type(self.handler.dv_c["4"][0].chall).__name__, "DnsChall") + self.assertTrue(isinstance(self.handler.dv_c["0"][0].chall, + challenge_util.DnsChall)) + self.assertTrue(isinstance(self.handler.dv_c["1"][0].chall, + challenge_util.DvsniChall)) + self.assertTrue(isinstance(self.handler.dv_c["2"][0].chall, + challenge_util.SimpleHttpsChall)) + self.assertTrue(isinstance(self.handler.dv_c["3"][0].chall, + challenge_util.SimpleHttpsChall)) + self.assertTrue(isinstance(self.handler.dv_c["4"][0].chall, + challenge_util.DnsChall)) + + self.assertTrue(isinstance(self.handler.client_c["2"][0].chall, + challenge_util.PopChall)) + self.assertTrue(isinstance(self.handler.client_c["4"][0].chall, + challenge_util.RecTokenChall)) + + @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") + def test_perform_exception_cleanup(self, mock_chall_path): + """3 Challenge messages... fail perform... clean up.""" + # pylint: disable=protected-access + self.mock_dv_auth.perform.side_effect = errors.LetsEncryptDvsniError + + challenges = acme_util.get_challenges() + combos = acme_util.gen_combos(challenges) + + for i in xrange(3): + self.handler.add_chall_msg( + str(i), + acme_util.get_chall_msg( + str(i), "nonce%d" % i, challenges, combos), + "dummy_key") + + mock_chall_path.return_value = gen_path( + ["dvsni", "proofOfPossession"], challenges) + + # This may change in the future... but for now catch the error + self.assertRaises(errors.LetsEncryptAuthHandlerError, + self.handler._satisfy_challenges) + + # Verify cleanup is actually run correctly + self.assertEqual(self.mock_dv_auth.cleanup.call_count, 3) + self.assertEqual(self.mock_client_auth.cleanup.call_count, 3) + + # Check DV cleanup + mock_cleanup_args = self.mock_dv_auth.cleanup.call_args_list + for i in xrange(3): + # Assert length of arg list was 1 + arg_chall_list = mock_cleanup_args[i][0][0] + self.assertEqual(len(arg_chall_list), 1) + self.assertTrue(isinstance(arg_chall_list[0], + challenge_util.DvsniChall)) + + # Check Auth cleanup + mock_cleanup_args = self.mock_client_auth.cleanup.call_args_list + for i in xrange(3): + arg_chall_list = mock_cleanup_args[i][0][0] + self.assertEqual(len(arg_chall_list), 1) + self.assertTrue(isinstance(arg_chall_list[0], + challenge_util.PopChall)) - self.assertEqual( - type(self.handler.client_c["2"][0].chall).__name__, "PopChall") - self.assertEqual( - type(self.handler.client_c["4"][0].chall).__name__, "RecTokenChall") def _get_exp_response(self, domain, path, challenges): # pylint: disable=no-self-use exp_resp = ["null"] * len(challenges) @@ -259,7 +310,7 @@ class GetAuthorizationsTest(unittest.TestCase): def test_solved3_at_once(self): # Set 3 DVSNI challenges challenge = [acme_util.CHALLENGES["dvsni"]] - for i in range(3): + for i in xrange(3): self.handler.add_chall_msg( str(i), acme_util.get_chall_msg(str(i), "nonce%d" % i, challenge), @@ -277,7 +328,7 @@ class GetAuthorizationsTest(unittest.TestCase): self._test_finished() def _sat_solved_at_once(self): - for i in range(3): + for i in xrange(3): dom = str(i) self.handler.responses[dom] = ["DvsniChall%d" % i] self.handler.paths[dom] = [0] @@ -314,7 +365,7 @@ class GetAuthorizationsTest(unittest.TestCase): challs = [] challs.append(acme_util.get_challenges()) challs.append(acme_util.get_dv_challenges()) - for i in range(2): + for i in xrange(2): dom = str(i) self.handler.add_chall_msg( dom, @@ -388,7 +439,7 @@ class PathSatisfiedTest(unittest.TestCase): self.handler.paths[dom[4]] = [] self.handler.responses[dom[4]] = ["respond... sure"] - for i in range(5): + for i in xrange(5): self.assertTrue(self.handler._path_satisfied(dom[i])) def test_not_satisfied(self): @@ -405,16 +456,25 @@ class PathSatisfiedTest(unittest.TestCase): self.handler.paths[dom[3]] = [0] self.handler.responses[dom[3]] = ["null"] - for i in range(4): + for i in xrange(4): self.assertFalse(self.handler._path_satisfied(dom[i])) -def gen_auth_resp(chall_list): # pylint: disable=missing-docstring +def gen_auth_resp(chall_list): + """Generate a dummy authorization response.""" return ["%s%s" % (type(chall).__name__, chall.domain) for chall in chall_list] -def gen_path(str_list, challenges): # pylint: disable=missing-docstring +def gen_path(str_list, challenges): + """Generate a path for challenge messages + + :param list str_list: challenge message types (:class:`str`) + :param dict challenges: ACME challenge messages + + :return: :class:`list` of :class:`int` + + """ path = [] for i, chall in enumerate(challenges): for str_chall in str_list: diff --git a/letsencrypt/client/tests/challenge_util_test.py b/letsencrypt/client/tests/challenge_util_test.py index 7ce4d5aef..88ec66a19 100644 --- a/letsencrypt/client/tests/challenge_util_test.py +++ b/letsencrypt/client/tests/challenge_util_test.py @@ -5,7 +5,6 @@ import re import unittest import M2Crypto -import mock from letsencrypt.client import challenge_util from letsencrypt.client import CONFIG @@ -18,32 +17,19 @@ class DvsniGenCertTest(unittest.TestCase): def test_standard(self): """Basic test for straightline code.""" - # This is a helper function that can be used for handling - # open context managers more elegantly. It avoids dealing with - # __enter__ and __exit__ calls. - # http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open - m_open = mock.mock_open() - with mock.patch("letsencrypt.client.challenge_util.open", - m_open, create=True): + domain = "example.com" + dvsni_r = "r_value" + r_b64 = le_util.jose_b64encode(dvsni_r) + pem = pkg_resources.resource_string( + __name__, os.path.join("testdata", "rsa256_key.pem")) + key = client.Client.Key("path", pem) + nonce = "12345ABCDE" + cert_pem, s_b64 = self._call(domain, r_b64, nonce, key) - domain = "example.com" - dvsni_r = "r_value" - r_b64 = le_util.jose_b64encode(dvsni_r) - pem = pkg_resources.resource_string( - __name__, os.path.join("testdata", "rsa256_key.pem")) - key = le_util.Key("path", pem) - nonce = "12345ABCDE" - s_b64 = self._call("tmp.crt", domain, r_b64, nonce, key) - - self.assertTrue(m_open.called) - self.assertEqual(m_open.call_args[0], ("tmp.crt", 'w')) - self.assertEqual(m_open().write.call_count, 1) - - # pylint: disable=protected-access - ext = challenge_util._dvsni_gen_ext( - dvsni_r, le_util.jose_b64decode(s_b64)) - self._standard_check_cert( - m_open().write.call_args[0][0], domain, nonce, ext) + # pylint: disable=protected-access + ext = challenge_util._dvsni_gen_ext( + dvsni_r, le_util.jose_b64decode(s_b64)) + self._standard_check_cert(cert_pem, domain, nonce, ext) def _standard_check_cert(self, pem, domain, nonce, ext): """Check the certificate fields.""" @@ -59,7 +45,7 @@ class DvsniGenCertTest(unittest.TestCase): self.assertEqual(exp_sans, act_sans) - # pylint: disable= no-self-use - def _call(self, filepath, name, r_b64, nonce, key): + @classmethod + def _call(cls, name, r_b64, nonce, key): from letsencrypt.client.challenge_util import dvsni_gen_cert - return dvsni_gen_cert(filepath, name, r_b64, nonce, key) + return dvsni_gen_cert(name, r_b64, nonce, key) diff --git a/letsencrypt/client/tests/client_test.py b/letsencrypt/client/tests/client_test.py index a7d4d1148..f7f97dc87 100644 --- a/letsencrypt/client/tests/client_test.py +++ b/letsencrypt/client/tests/client_test.py @@ -2,17 +2,17 @@ import unittest import mock -import zope.component + +from letsencrypt.client import errors class RollbackTest(unittest.TestCase): """Test the rollback function.""" def setUp(self): self.m_install = mock.MagicMock() - self.m_input = mock.MagicMock() - zope.component.getUtility = self.m_input - def _call(self, checkpoints): # pylint: disable=no-self-use + @classmethod + def _call(cls, checkpoints): from letsencrypt.client.client import rollback rollback(checkpoints) @@ -25,11 +25,11 @@ class RollbackTest(unittest.TestCase): self.assertEqual(self.m_install().rollback_checkpoints.call_count, 1) self.assertEqual(self.m_install().restart.call_count, 1) + @mock.patch("letsencrypt.client.client.zope.component.getUtility") @mock.patch("letsencrypt.client.reverter.Reverter") @mock.patch("letsencrypt.client.client.determine_installer") - def test_misconfiguration_fixed(self, mock_det, mock_rev): - from letsencrypt.client.errors import LetsEncryptMisconfigurationError - mock_det.side_effect = [LetsEncryptMisconfigurationError, + def test_misconfiguration_fixed(self, mock_det, mock_rev, mock_input): + mock_det.side_effect = [errors.LetsEncryptMisconfigurationError, self.m_install] self.m_input().yesno.return_value = True @@ -42,12 +42,13 @@ class RollbackTest(unittest.TestCase): # Only restart once self.assertEqual(self.m_install.restart.call_count, 1) + @mock.patch("letsencrypt.client.client.zope.component.getUtility") @mock.patch("letsencrypt.client.client.logging.warning") @mock.patch("letsencrypt.client.reverter.Reverter") @mock.patch("letsencrypt.client.client.determine_installer") - def test_misconfiguration_remains(self, mock_det, mock_rev, mock_warn): - from letsencrypt.client.errors import LetsEncryptMisconfigurationError - mock_det.side_effect = LetsEncryptMisconfigurationError + def test_misconfiguration_remains( + self, mock_det, mock_rev, mock_warn, mock_input): + mock_det.side_effect = errors.LetsEncryptMisconfigurationError self.m_input().yesno.return_value = True @@ -62,11 +63,12 @@ class RollbackTest(unittest.TestCase): # There should be a warning about the remaining problem self.assertEqual(mock_warn.call_count, 1) + @mock.patch("letsencrypt.client.client.zope.component.getUtility") @mock.patch("letsencrypt.client.reverter.Reverter") @mock.patch("letsencrypt.client.client.determine_installer") - def test_user_decides_to_manually_investigate(self, mock_det, mock_rev): - from letsencrypt.client.errors import LetsEncryptMisconfigurationError - mock_det.side_effect = LetsEncryptMisconfigurationError + def test_user_decides_to_manually_investigate( + self, mock_det, mock_rev, mock_input): + mock_det.side_effect = errors.LetsEncryptMisconfigurationError self.m_input().yesno.return_value = False diff --git a/letsencrypt/client/tests/crypto_util_test.py b/letsencrypt/client/tests/crypto_util_test.py index 96acdbd9b..8b1a8ecd7 100644 --- a/letsencrypt/client/tests/crypto_util_test.py +++ b/letsencrypt/client/tests/crypto_util_test.py @@ -95,7 +95,7 @@ class CSRMatchesPubkeyTest(unittest.TestCase): self.assertFalse(self._call_testdata('csr.pem', RSA512_KEY)) -class MakeKeyTest(unittest.TestCase): +class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Tests for letsencrypt.client.crypto_util.make_key.""" def test_it(self): # pylint: disable=no-self-use @@ -124,6 +124,7 @@ class ValidPrivkeyTest(unittest.TestCase): class MakeSSCertTest(unittest.TestCase): + # pylint: disable=too-few-public-methods """Tests for letsencrypt.client.crypto_util.make_ss_cert.""" def test_it(self): # pylint: disable=no-self-use @@ -170,6 +171,7 @@ class GetCertInfoTest(unittest.TestCase): class B64CertToPEMTest(unittest.TestCase): + # pylint: disable=too-few-public-methods """Tests for letsencrypt.client.crypto_util.b64_cert_to_pem.""" def test_it(self): diff --git a/letsencrypt/client/tests/reverter_test.py b/letsencrypt/client/tests/reverter_test.py index 7c9635d33..39ef3d135 100644 --- a/letsencrypt/client/tests/reverter_test.py +++ b/letsencrypt/client/tests/reverter_test.py @@ -7,6 +7,8 @@ import unittest import mock +from letsencrypt.client import errors + class ReverterCheckpointLocalTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes @@ -29,6 +31,8 @@ class ReverterCheckpointLocalTest(unittest.TestCase): shutil.rmtree(self.dir1) shutil.rmtree(self.dir2) + logging.disable(logging.NOTSET) + def test_basic_add_to_temp_checkpoint(self): # These shouldn't conflict even though they are both named config.txt self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") @@ -44,20 +48,16 @@ class ReverterCheckpointLocalTest(unittest.TestCase): "{0}\n{1}\n".format(self.config1, self.config2)) def test_add_to_checkpoint_copy_failure(self): - from letsencrypt.client.errors import LetsEncryptReverterError - with mock.patch("letsencrypt.client.reverter." "shutil.copy2") as mock_copy2: mock_copy2.side_effect = IOError("bad copy") - self.assertRaises(LetsEncryptReverterError, + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.add_to_checkpoint, self.sets[0], "save1") def test_checkpoint_conflict(self): """Make sure that checkpoint errors are thrown appropriately.""" - from letsencrypt.client.errors import LetsEncryptReverterError - config3 = os.path.join(self.dir1, "config3.txt") self.reverter.register_file_creation(True, config3) update_file(config3, "This is a new file!") @@ -67,14 +67,14 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.reverter.add_to_temp_checkpoint(self.sets[0], "save2") # Raise error self.assertRaises( - LetsEncryptReverterError, self.reverter.add_to_checkpoint, + errors.LetsEncryptReverterError, self.reverter.add_to_checkpoint, self.sets[2], "save3") # Should not cause an error self.reverter.add_to_checkpoint(self.sets[1], "save4") # Check to make sure new files are also checked... self.assertRaises( - LetsEncryptReverterError, + errors.LetsEncryptReverterError, self.reverter.add_to_checkpoint, set([config3]), "invalid save") @@ -118,79 +118,70 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.assertEqual(len(files), 1) def test_register_file_creation_write_error(self): - from letsencrypt.client.errors import LetsEncryptReverterError - m_open = mock.mock_open() with mock.patch("letsencrypt.client.reverter.open", m_open, create=True): m_open.side_effect = OSError("bad open") - self.assertRaises(LetsEncryptReverterError, + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.register_file_creation, True, self.config1) def test_bad_registration(self): - from letsencrypt.client.errors import LetsEncryptReverterError # Made this mistake and want to make sure it doesn't happen again... - self.assertRaises(LetsEncryptReverterError, + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.register_file_creation, "filepath") def test_recovery_routine_in_progress_failure(self): - from letsencrypt.client.errors import LetsEncryptReverterError self.reverter.add_to_checkpoint(self.sets[0], "perm save") # pylint: disable=protected-access self.reverter._recover_checkpoint = mock.MagicMock( - side_effect=LetsEncryptReverterError) - self.assertRaises(LetsEncryptReverterError, + side_effect=errors.LetsEncryptReverterError) + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.recovery_routine) def test_recover_checkpoint_revert_temp_failures(self): # pylint: disable=invalid-name - from letsencrypt.client.errors import LetsEncryptReverterError + mock_recover = mock.MagicMock( + side_effect=errors.LetsEncryptReverterError("e")) - mock_recover = mock.MagicMock(side_effect=LetsEncryptReverterError("e")) # pylint: disable=protected-access self.reverter._recover_checkpoint = mock_recover self.reverter.add_to_temp_checkpoint(self.sets[0], "config1 save") - self.assertRaises(LetsEncryptReverterError, + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rollback_failure(self): - from letsencrypt.client.errors import LetsEncryptReverterError - - mock_recover = mock.MagicMock(side_effect=LetsEncryptReverterError("e")) + mock_recover = mock.MagicMock( + side_effect=errors.LetsEncryptReverterError("e")) # pylint: disable=protected-access self.reverter._recover_checkpoint = mock_recover self.reverter.add_to_checkpoint(self.sets[0], "config1 save") self.reverter.finalize_checkpoint("Title") - self.assertRaises(LetsEncryptReverterError, + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.rollback_checkpoints, 1) def test_recover_checkpoint_copy_failure(self): - from letsencrypt.client.errors import LetsEncryptReverterError - self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") with mock.patch("letsencrypt.client.reverter.shutil." "copy2") as mock_copy2: mock_copy2.side_effect = OSError("bad copy") - self.assertRaises(LetsEncryptReverterError, + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rm_failure(self): - from letsencrypt.client.errors import LetsEncryptReverterError - self.reverter.add_to_temp_checkpoint(self.sets[0], "temp save") with mock.patch("letsencrypt.client.reverter.shutil." "rmtree") as mock_rmtree: mock_rmtree.side_effect = OSError("Cannot remove tree") - self.assertRaises(LetsEncryptReverterError, + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.revert_temporary_config) @mock.patch("letsencrypt.client.reverter.logging.warning") @@ -202,11 +193,9 @@ class ReverterCheckpointLocalTest(unittest.TestCase): @mock.patch("letsencrypt.client.reverter.os.remove") def test_recover_checkpoint_remove_failure(self, mock_remove): - from letsencrypt.client.errors import LetsEncryptReverterError - self.reverter.register_file_creation(True, self.config1) mock_remove.side_effect = OSError("Can't remove") - self.assertRaises(LetsEncryptReverterError, + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.revert_temporary_config) def test_recovery_routine_temp_and_perm(self): @@ -262,16 +251,17 @@ class TestFullCheckpointsReverter(unittest.TestCase): shutil.rmtree(self.dir1) shutil.rmtree(self.dir2) + logging.disable(logging.NOTSET) + def test_rollback_improper_inputs(self): - from letsencrypt.client.errors import LetsEncryptReverterError self.assertRaises( - LetsEncryptReverterError, + errors.LetsEncryptReverterError, self.reverter.rollback_checkpoints, "-1") self.assertRaises( - LetsEncryptReverterError, + errors.LetsEncryptReverterError, self.reverter.rollback_checkpoints, -1000) self.assertRaises( - LetsEncryptReverterError, + errors.LetsEncryptReverterError, self.reverter.rollback_checkpoints, "one") def test_rollback_finalize_checkpoint_valid_inputs(self): @@ -311,24 +301,20 @@ class TestFullCheckpointsReverter(unittest.TestCase): @mock.patch("letsencrypt.client.reverter.shutil.move") def test_finalize_checkpoint_cannot_title(self, mock_move): - from letsencrypt.client.errors import LetsEncryptReverterError - self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_move.side_effect = OSError("cannot move") - self.assertRaises(LetsEncryptReverterError, + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.finalize_checkpoint, "Title") @mock.patch("letsencrypt.client.reverter.os.rename") def test_finalize_checkpoint_no_rename_directory(self, mock_rename): # pylint: disable=invalid-name - from letsencrypt.client.errors import LetsEncryptReverterError - self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_rename.side_effect = OSError - self.assertRaises(LetsEncryptReverterError, + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.finalize_checkpoint, "Title") @@ -345,24 +331,28 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.assertEqual(read_in(self.config2), "directive-dir2") self.assertFalse(os.path.isfile(config3)) - def test_view_config_changes(self): + @mock.patch("letsencrypt.client.client.zope.component.getUtility") + def test_view_config_changes(self, mock_output): """This is not strict as this is subject to change.""" self._setup_three_checkpoints() - # Just make sure it doesn't throw any errors. + + # Make sure it doesn't throw any errors self.reverter.view_config_changes() + # Make sure notification is output + self.assertEqual(mock_output().generic_notification.call_count, 1) + @mock.patch("letsencrypt.client.reverter.logging") def test_view_config_changes_no_backups(self, mock_logging): self.reverter.view_config_changes() self.assertTrue(mock_logging.info.call_count > 0) def test_view_config_changes_bad_backups_dir(self): - from letsencrypt.client.errors import LetsEncryptReverterError # There shouldn't be any "in progess directories when this is called # It must just be clean checkpoints os.makedirs(os.path.join(self.direc['backup'], "in_progress")) - self.assertRaises(LetsEncryptReverterError, + self.assertRaises(errors.LetsEncryptReverterError, self.reverter.view_config_changes) def _setup_three_checkpoints(self): @@ -395,6 +385,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): class QuickInitReverterTest(unittest.TestCase): + # pylint: disable=too-few-public-methods """Quick test of init.""" def test_init(self): from letsencrypt.client.reverter import Reverter diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index c133c98f3..ae6be821a 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -1,5 +1,9 @@ #!/usr/bin/env python -"""Parse command line and call the appropriate functions.""" +"""Parse command line and call the appropriate functions. + +..todo:: Sanity check all input. Be sure to avoid shell code etc... + +""" import argparse import logging import os @@ -8,6 +12,7 @@ import sys import zope.component import zope.interface +import letsencrypt from letsencrypt.client import CONFIG from letsencrypt.client import client from letsencrypt.client import display @@ -20,7 +25,7 @@ from letsencrypt.client import log def main(): # pylint: disable=too-many-statements,too-many-branches """Command line argument parsing and main script execution.""" parser = argparse.ArgumentParser( - description="An ACME client that can update Apache configurations.") + description="letsencrypt client %s" % letsencrypt.__version__) parser.add_argument("-d", "--domains", dest="domains", metavar="DOMAIN", nargs="+") @@ -99,7 +104,7 @@ def main(): # pylint: disable=too-many-statements,too-many-branches except errors.LetsEncryptMisconfigurationError as err: logging.fatal("Please fix your configuration before proceeding. " "The Installer exited with the following message: " - "%s", str(err)) + "%s", err) sys.exit(1) # Use the same object if possible @@ -166,7 +171,6 @@ def get_all_names(installer): """ names = list(installer.get_all_names()) - client.sanity_check_names(names) if not names: logging.fatal("No domain names were found in your installation") @@ -178,7 +182,6 @@ def get_all_names(installer): return names - def read_file(filename): """Returns the given file's contents with universal new line support. diff --git a/setup.py b/setup.py index 4f192b886..5501c7dd6 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,26 @@ #!/usr/bin/env python +import codecs +import os +import re + from setuptools import setup +def read_file(filename, encoding='utf8'): + """Read unicode from given file.""" + with codecs.open(filename, encoding=encoding) as fd: + return fd.read() + + +here = os.path.abspath(os.path.dirname(__file__)) + +# read version number (and other metadata) from package init +init_fn = os.path.join(here, 'letsencrypt', '__init__.py') +meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", read_file(init_fn))) + +readme = read_file(os.path.join(here, 'README.rst')) +changes = read_file(os.path.join(here, 'CHANGES.rst')) + install_requires = [ 'argparse', 'jsonschema', @@ -18,6 +37,7 @@ install_requires = [ ] docs_extras = [ + 'repoze.sphinx.autointerface', 'Sphinx', ] @@ -25,15 +45,15 @@ testing_extras = [ 'coverage', 'nose', 'nosexcover', - 'pylint<1.4', # py2.6 compat, c.f #97 - 'astroid<1.3.0', # py2.6 compat, c.f. #187 + 'pylint>=1.4.0', # upstream #248 'tox', ] setup( name="letsencrypt", - version="0.1", + version=meta['version'], description="Let's Encrypt", + long_description=readme, # later: + '\n\n' + changes author="Let's Encrypt Project", license="", url="https://letsencrypt.org", diff --git a/tox.ini b/tox.ini index c8c671ca1..4049c78a0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. +# Tox (http://tox.testrun.org/) is a tool for running tests in +# multiple virtualenvs. To use it, "pip install tox" and then run +# "tox" from this directory. [tox] envlist = py26,py27,cover,lint @@ -12,11 +11,14 @@ commands = python setup.py test -q # -q does not suppress errors [testenv:cover] +basepython = python2.7 commands = python setup.py dev - python setup.py nosetests --with-coverage --cover-min-percentage=61 + python setup.py nosetests --with-coverage --cover-min-percentage=66 [testenv:lint] +# recent versions of pylint do not support Python 2.6 (#97, #187) +basepython = python2.7 commands = python setup.py dev pylint --rcfile=.pylintrc letsencrypt