From 0a14007db229000e0901ccd1c3cc188f5749f7e8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 25 Jan 2015 21:30:24 +0100 Subject: [PATCH 01/35] refactor docs, please check - README has only the most important infos that a new reader needs in his first minute of contact with the project (to decide whether it is interesting or not) - CHANGES shall later be a curated change log (== important changes between releases) - separate docs into intro, using, project - intro docs = include README, CHANGES (avoid duplication) --- .travis.yml | 2 +- CHANGES.rst | 27 +++++++++ README.md | 143 ----------------------------------------------- README.rst | 78 ++++++++++++++++++++++++++ docs/index.rst | 19 ++----- docs/intro.rst | 6 ++ docs/project.rst | 78 ++++++++++++++++++++++++++ docs/using.rst | 58 +++++++++++++++++++ 8 files changed, 254 insertions(+), 157 deletions(-) create mode 100644 CHANGES.rst delete mode 100644 README.md create mode 100644 README.rst create mode 100644 docs/intro.rst create mode 100644 docs/project.rst create mode 100644 docs/using.rst diff --git a/.travis.yml b/.travis.yml index 54ed84263..bed4f6d30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -# To mimic README.md installation and hacking instructions as much as +# To mimic README.rst 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 diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 000000000..6ef5ee373 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,27 @@ +ChangeLog +========= + +Please note: the change log will only get updated after first release. + +Until then please use commit log: https://github.com/letsencrypt/lets-encrypt-preview/commits/master + + +Release 0.1.0 (not released yet) +-------------------------------- + +New Features: + +* ... + +Fixes: + +* ... + +Other changes: + +* ... + +Release 0.0.0 (not released yet) +-------------------------------- + +Initial release. 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..07f98dfca --- /dev/null +++ b/README.rst @@ -0,0 +1,78 @@ +About the Let's Encrypt Client +============================== + +In short: getting and installing SSL/TLS certificates made easy. + +The Let's Encrypt Client is a tool that talks to the Let's Encrypt CA +so you can comfortably and quickly get trusted TLS certificates that just +work without warnings in every browser. + +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 + + +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.** + + +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 can be rolled back N checkpoints +* 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/index.rst b/docs/index.rst index c636507df..0387269ab 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,18 +1,12 @@ -.. 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 Indices and tables @@ -21,4 +15,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..57010b8e2 --- /dev/null +++ b/docs/project.rst @@ -0,0 +1,78 @@ +================================ +The Let's Encrypt Client Project +================================ + +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. + + +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: + +:: + + def foo(arg): + """Short description. + + :param int arg: Some number. + + :returns: Argument + :rtype: int + + """ + return arg + +3. Remember to use `./venv/bin/pylint`. + + +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. + +API documentation +================= + +.. toctree:: + :glob: + + api/** diff --git a/docs/using.rst b/docs/using.rst new file mode 100644 index 000000000..cda2d7ec4 --- /dev/null +++ b/docs/using.rst @@ -0,0 +1,58 @@ +============================== +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] +(https://github.com/letsencrypt/lets-encrypt-preview/issues/68)). + +Therefore, prerequisites for other platforms listed below are provided +mainly for the [developers](#hacking) reference. + +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 + + +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/ From f9d968071ebd63d515a0178de667f725be455f9f Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 26 Jan 2015 00:33:58 -0800 Subject: [PATCH 02/35] Document raises reverter error in rollback func --- letsencrypt/client/reverter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/letsencrypt/client/reverter.py b/letsencrypt/client/reverter.py index 8342ecd06..b119d1ba6 100644 --- a/letsencrypt/client/reverter.py +++ b/letsencrypt/client/reverter.py @@ -44,6 +44,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) From ae4c16065481def2e67fb119e26337af4913d3c4 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 26 Jan 2015 01:07:50 -0800 Subject: [PATCH 03/35] import errors at the top --- letsencrypt/client/tests/reverter_test.py | 67 ++++++++--------------- 1 file changed, 24 insertions(+), 43 deletions(-) diff --git a/letsencrypt/client/tests/reverter_test.py b/letsencrypt/client/tests/reverter_test.py index 7c9635d33..6049110fc 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 @@ -44,20 +46,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 +65,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 +116,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 +191,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): @@ -263,15 +250,14 @@ class TestFullCheckpointsReverter(unittest.TestCase): shutil.rmtree(self.dir2) 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 +297,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") @@ -357,12 +339,11 @@ class TestFullCheckpointsReverter(unittest.TestCase): 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): From 73b95c43072924fe29f80b90d34fbf83729b5583 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 26 Jan 2015 01:27:00 -0800 Subject: [PATCH 04/35] Fix based on comments --- letsencrypt/client/tests/client_test.py | 23 +++++++++++++---------- letsencrypt/client/tests/reverter_test.py | 3 +++ letsencrypt/scripts/main.py | 4 ++-- tox.ini | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/letsencrypt/client/tests/client_test.py b/letsencrypt/client/tests/client_test.py index bad6eee26..8e29d53c8 100644 --- a/letsencrypt/client/tests/client_test.py +++ b/letsencrypt/client/tests/client_test.py @@ -2,17 +2,15 @@ import unittest import mock -import zope.component 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,13 +23,14 @@ 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): + def test_misconfiguration_fixed(self, mock_det, mock_rev, mock_input): from letsencrypt.client.errors import LetsEncryptMisconfigurationError mock_det.side_effect = [LetsEncryptMisconfigurationError, self.m_install] - self.m_input().generic_yesno.return_value = True + mock_input().generic_yesno.return_value = True self._call(1) @@ -42,14 +41,16 @@ 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): + def test_misconfiguration_remains( + self, mock_det, mock_rev, mock_warn, mock_input): from letsencrypt.client.errors import LetsEncryptMisconfigurationError mock_det.side_effect = LetsEncryptMisconfigurationError - self.m_input().generic_yesno.return_value = True + mock_input().generic_yesno.return_value = True self._call(1) @@ -62,13 +63,15 @@ 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): + def test_user_decides_to_manually_investigate( + self, mock_det, mock_rev, mock_input): from letsencrypt.client.errors import LetsEncryptMisconfigurationError mock_det.side_effect = LetsEncryptMisconfigurationError - self.m_input().generic_yesno.return_value = False + mock_input().generic_yesno.return_value = False self._call(1) diff --git a/letsencrypt/client/tests/reverter_test.py b/letsencrypt/client/tests/reverter_test.py index 6049110fc..553c9d946 100644 --- a/letsencrypt/client/tests/reverter_test.py +++ b/letsencrypt/client/tests/reverter_test.py @@ -31,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") @@ -245,6 +247,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.config1, self.config2, self.dir1, self.dir2, self.sets = tup def tearDown(self): + shutil.rmtree(self.work_dir) shutil.rmtree(self.dir1) shutil.rmtree(self.dir2) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 95c5cc8b6..4dfa70764 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -11,8 +11,8 @@ import zope.interface from letsencrypt.client import CONFIG from letsencrypt.client import client from letsencrypt.client import display -from letsencrypt.client import interfaces from letsencrypt.client import errors +from letsencrypt.client import interfaces from letsencrypt.client import log @@ -98,7 +98,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 diff --git a/tox.ini b/tox.ini index c8c671ca1..190bfd2d7 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=61 + python setup.py nosetests --with-coverage --cover-min-percentage=66 [testenv:lint] commands = From 2db2060f851bf6a3823a0cf43deb77e4d64411b4 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 26 Jan 2015 02:18:15 -0800 Subject: [PATCH 05/35] restore logging in tearDown --- letsencrypt/client/tests/reverter_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt/client/tests/reverter_test.py b/letsencrypt/client/tests/reverter_test.py index 553c9d946..3213bfea5 100644 --- a/letsencrypt/client/tests/reverter_test.py +++ b/letsencrypt/client/tests/reverter_test.py @@ -247,11 +247,12 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.config1, self.config2, self.dir1, self.dir2, self.sets = tup def tearDown(self): - shutil.rmtree(self.work_dir) shutil.rmtree(self.dir1) shutil.rmtree(self.dir2) + logging.disable(logging.NOTSET) + def test_rollback_improper_inputs(self): self.assertRaises( errors.LetsEncryptReverterError, From 243cc4f9fb6e9d9b570cf1834299adb5d8dbb8df Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 26 Jan 2015 04:49:40 -0800 Subject: [PATCH 06/35] Fixed print, fixed logging, made display work --- letsencrypt/client/display.py | 268 ++++++++++++++++++---- letsencrypt/client/reverter.py | 22 +- letsencrypt/client/tests/reverter_test.py | 9 +- 3 files changed, 240 insertions(+), 59 deletions(-) diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index 867675495..fa8b43ce3 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -1,4 +1,5 @@ """Lets Encrypt display.""" +import os import textwrap import dialog @@ -14,7 +15,13 @@ HEIGHT = 20 class CommonDisplayMixin(object): # pylint: disable=too-few-public-methods """Mixin with methods common to classes implementing IDisplay.""" - def redirect_by_default(self): # pylint: disable=missing-docstring + def redirect_by_default(self): + """Determines whether the user would like to redirect to HTTPS. + + :returns: True if redirect is desired, False otherwise + :rtype: bool + + """ choices = [ ("Easy", "Allow both HTTP and HTTPS access to these sites"), ("Secure", "Make all requests redirect to secure HTTPS access")] @@ -41,12 +48,29 @@ class NcursesDisplay(CommonDisplayMixin): self.width = width self.height = height - def generic_notification(self, message): - # pylint: disable=missing-docstring - self.dialog.msgbox(message, width=self.width) + def generic_notification(self, message, height=10): + """Display a notification to the user and wait for user acceptance. + + :param str message: Message to display + :param int height: Height of the dialog box + + """ + self.dialog.msgbox(message, height, width=self.width) def generic_menu(self, message, choices, unused_input_text=""): - # pylint: disable=missing-docstring + """Display a menu. + + :param str message: title of menu + :param choices: Menu lines + :type choices: list of tuples (tag, item) or + list of items (tags will be enumerated) + + :returns: tuple of the form (code, tag) where + code is a display exit code + tag is the tag string corresponding to the item chosen + :rtype: tuple + + """ # Can accept either tuples or just the actual choices if choices and isinstance(choices[0], tuple): code, selection = self.dialog.menu( @@ -57,18 +81,46 @@ class NcursesDisplay(CommonDisplayMixin): code, tag = self.dialog.menu( message, choices=choices, width=self.width, height=self.height) - return code(int(tag) - 1) + return code, int(tag) - 1 - def generic_input(self, message): # pylint: disable=missing-docstring + def generic_input(self, message): + """Display an input box to the user. + + :param str message: Message to display that asks for input. + + :returns: tuple of the form (code, string) where + code is a display exit code + string is the input entered by the user + + """ return self.dialog.inputbox(message) def generic_yesno(self, message, yes_label="Yes", no_label="No"): - # pylint: disable=missing-docstring + """Display a Yes/No dialog box + + :param str message: message to display to user + :param str yes_label: label on the 'yes' button + :param str no_label: label on the 'no' button + + :returns: if yes_label was selected + :rtype: bool + + """ return self.dialog.DIALOG_OK == self.dialog.yesno( message, self.height, self.width, yes_label=yes_label, no_label=no_label) - def filter_names(self, names): # pylint: disable=missing-docstring + def filter_names(self, names): + """Determine which names the user would like to select from a list. + + :param list names: domain names + + :returns: tuple of the form (code, names) where + code is a display exit code + names is a list of names selected + :rtype: tuple + + """ choices = [(n, "", 0) for n in names] code, names = self.dialog.checklist( "Which names would you like to activate HTTPS for?", @@ -76,12 +128,26 @@ class NcursesDisplay(CommonDisplayMixin): return code, [str(s) for s in names] def success_installation(self, domains): - # pylint: disable=missing-docstring + """Display a box confirming the installation of HTTPS. + + :param list domains: domain names which were enabled + + """ self.dialog.msgbox( "\nCongratulations! You have successfully enabled " + gen_https_names(domains) + "!", width=self.width) - def display_certs(self, certs): # pylint: disable=missing-docstring + def display_certs(self, certs): + """Display certificates for revocation. + + :param list certs: `list` of `dict` used throughout revoker.py + + :returns: tuple of the form (code, selection) where + code is a display exit code + selection is the user's int selection + :rtype: tuple + + """ list_choices = [ (str(i+1), "%s | %s | %s" % (str(c["cn"].ljust(self.width - 39)), @@ -98,7 +164,15 @@ class NcursesDisplay(CommonDisplayMixin): tag = -1 return code, (int(tag) - 1) - def confirm_revocation(self, cert): # pylint: disable=missing-docstring + def confirm_revocation(self, cert): + """Confirm revocation screen. + + :param dict cert: cert dict used throughout revoker.py + + :returns: True if user would like to revoke, False otherwise + :rtype: bool + + """ text = ("Are you sure you would like to revoke the following " "certificate:\n") text += cert_info_frame(cert) @@ -106,10 +180,14 @@ class NcursesDisplay(CommonDisplayMixin): return self.dialog.DIALOG_OK == self.dialog.yesno( text, width=self.width, height=self.height) - def more_info_cert(self, cert): # pylint: disable=missing-docstring + def more_info_cert(self, cert): + """Displays more information about the certificate. + + :param dict cert: cert dict used throughout revoker.py + + """ text = "Certificate Information:\n" text += cert_info_frame(cert) - print text self.dialog.msgbox(text, width=self.width, height=self.height) @@ -122,15 +200,36 @@ class FileDisplay(CommonDisplayMixin): super(FileDisplay, self).__init__() self.outfile = outfile - def generic_notification(self, message): - # pylint: disable=missing-docstring + def generic_notification(self, message, unused_height): + """Displays a notification and waits for user acceptance. + + :param str message: Message to display + + """ side_frame = '-' * 79 - msg = textwrap.fill(message, 80) - self.outfile.write("\n%s\n%s\n%s\n" % (side_frame, msg, side_frame)) + lines = message.splitlines() + fixed_l = [] + for line in lines: + fixed_l.append(textwrap.fill(line, 80)) + self.outfile.write( + "{0}{1}{0}{2}{0}{1}{0}".format( + os.linesep, side_frame, os.linesep.join(fixed_l))) raw_input("Press Enter to Continue") def generic_menu(self, message, choices, input_text=""): - # pylint: disable=missing-docstring + """Display a menu. + + :param str message: title of menu + :param choices: Menu lines + :type choices: list of tuples (tag, item) or + list of items (tags will be enumerated) + + :returns: tuple of the form (code, tag) where + code is a display exit code + tag is the tag string corresponding to the item chosen + :rtype: tuple + + """ # 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] @@ -145,27 +244,54 @@ class FileDisplay(CommonDisplayMixin): self.outfile.write("%s\n" % side_frame) - code, selection = self.__get_valid_int_ans( + code, selection = self._get_valid_int_ans( "%s (c to cancel): " % input_text) return code, (selection - 1) def generic_input(self, message): - # pylint: disable=no-self-use,missing-docstring + # pylint: disable=no-self-use + """Accept input from the user + + :param str message: message to display to the user + + :returns: tuple of (code, input) where + code is a display exit code + input is a str of the user's input + :rtype: tuple + + """ ans = raw_input("%s (Enter c to cancel)\n" % message) - if ans.startswith('c') or ans.startswith('C'): - return CANCEL, -1 + if ans == 'c' or ans == 'C': + return CANCEL, "-1" else: return OK, ans def generic_yesno(self, message, unused_yes_label="", unused_no_label=""): - # pylint: disable=missing-docstring + """Query the user with a yes/no question. + + :param str message: question for the user + + :returns: True for 'Yes', False for 'No" + :rtype: bool + + """ 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): # pylint: disable=missing-docstring + def filter_names(self, names): + """Determine which names the user would like to select from a list. + + :param list names: domain names + + :returns: tuple of the form (code, names) where + code is a display exit code + names is a list of names selected + :rtype: tuple + + """ code, tag = self.generic_menu( "Choose the names would you like to upgrade to HTTPS?", names, "Select the number of the name: ") @@ -173,7 +299,29 @@ class FileDisplay(CommonDisplayMixin): # Make sure to return a list... return code, [names[tag]] - def display_certs(self, certs): # pylint: disable=missing-docstring + def success_installation(self, domains): + """Display a box confirming the installation of HTTPS. + + :param list domains: domain names which were enabled + + """ + 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 display_certs(self, certs): + """Display certificates for revocation. + + :param list certs: `list` of `dict` used throughout revoker.py + + :returns: tuple of the form (code, selection) where + code is a display exit code + selection is the user's int selection + :rtype: tuple + + """ menu_choices = [(str(i+1), str(c["cn"]) + " - " + c["pub_key"] + " - " + str(c["not_before"])[:-6]) for i, c in enumerate(certs)] @@ -183,11 +331,20 @@ class FileDisplay(CommonDisplayMixin): self.outfile.write(textwrap.fill( "%s: %s - %s Signed (UTC): %s\n" % choice[:4])) - return self.__get_valid_int_ans("Revoke Number (c to cancel): ") - 1 + return self._get_valid_int_ans("Revoke Number (c to cancel): ") - 1 - def __get_valid_int_ans(self, input_string): + def _get_valid_int_ans(self, input_string): + """Get a numerical selection. + + :param str input_string: Instructions for the user to make a selection. + + :returns: tuple of the form (code, selection) where + code is a display exit code + selection is the user's int selection + :rtype: tuple + + """ valid_ans = False - e_msg = "Please input a number or the letter c to cancel\n" while not valid_ans: @@ -210,14 +367,15 @@ class FileDisplay(CommonDisplayMixin): return code, selection - 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): + """Confirm revocation screen. - def confirm_revocation(self, cert): # pylint: disable=missing-docstring + :param dict cert: cert dict used throughout revoker.py + + :returns: True if user would like to revoke, False otherwise + :rtype: bool + + """ self.outfile.write("Are you sure you would like to revoke " "the following certificate:\n") self.outfile.write(cert_info_frame(cert)) @@ -225,39 +383,49 @@ class FileDisplay(CommonDisplayMixin): ans = raw_input("y/n") return ans.startswith('y') or ans.startswith('Y') - def more_info_cert(self, cert): # pylint: disable=missing-docstring + def more_info_cert(self, cert): + """Displays more info about the cert. + + :param dict cert: cert dict used throughout revoker.py + + """ self.outfile.write("\nCertificate Information:\n") self.outfile.write(cert_info_frame(cert)) +# Display exit codes OK = "ok" CANCEL = "cancel" HELP = "help" -def cert_info_frame(cert): # pylint: disable=missing-docstring - text = "-" * (WIDTH - 4) + "\n" +def cert_info_frame(cert): + """Nicely frames a cert dict used in revoker.py""" + text = "-" * (WIDTH - 4) + os.linesep text += cert_info_string(cert) text += "-" * (WIDTH - 4) return text -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"] - text += "Public Key: %s\n" % cert["pub_key"] - text += "Not Before: %s\n" % str(cert["not_before"]) - text += "Not After: %s\n" % str(cert["not_after"]) - text += "Serial Number: %s\n" % cert["serial"] - text += "SHA1: %s\n" % cert["fingerprint"] - text += "Installed: %s\n" % cert["installed"] - return text +def cert_info_string(cert): + """Turn a cert dict into a string.""" + text = [] + text.append("Subject: %s" % cert["subject"]) + text.append("SAN: %s" % cert["san"]) + text.append("Issuer: %s" % cert["issuer"]) + text.append("Public Key: %s" % cert["pub_key"]) + text.append("Not Before: %s" % str(cert["not_before"])) + text.append("Not After: %s" % str(cert["not_after"])) + text.append("Serial Number: %s" % cert["serial"]) + text.append("SHA1: %s" % cert["fingerprint"]) + text.append("Installed: %s" % cert["installed"]) + return os.linesep.join(text) def gen_https_names(domains): """Returns a string of the https domains. Domains are formatted nicely with https:// prepended to each. + """ result = "" if len(domains) > 2: diff --git a/letsencrypt/client/reverter.py b/letsencrypt/client/reverter.py index b119d1ba6..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 @@ -100,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/tests/reverter_test.py b/letsencrypt/client/tests/reverter_test.py index 3213bfea5..e7766e331 100644 --- a/letsencrypt/client/tests/reverter_test.py +++ b/letsencrypt/client/tests/reverter_test.py @@ -331,12 +331,17 @@ 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() From fb2d8061c829c88c1b4db3cbe0b5ab4078b791e6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 26 Jan 2015 14:58:24 +0100 Subject: [PATCH 07/35] docs: markup fixes, separate section for api docs, link to demo video, improved phrasing --- CHANGES.rst | 6 ++--- README.rst | 14 +++++------ docs/api.rst | 8 +++++++ docs/index.rst | 5 ++++ docs/project.rst | 61 ++++++++++++++++++++++++------------------------ docs/using.rst | 10 ++++---- 6 files changed, 59 insertions(+), 45 deletions(-) create mode 100644 docs/api.rst diff --git a/CHANGES.rst b/CHANGES.rst index 6ef5ee373..741d9bc7c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,9 @@ ChangeLog ========= -Please note: the change log will only get updated after first release. - -Until then please use commit log: https://github.com/letsencrypt/lets-encrypt-preview/commits/master +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) diff --git a/README.rst b/README.rst index 07f98dfca..c66fcabf6 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ About the Let's Encrypt Client ============================== -In short: getting and installing SSL/TLS certificates made easy. +In short: getting and installing SSL/TLS certificates made easy (`watch demo video`_). The Let's Encrypt Client is a tool that talks to the Let's Encrypt CA so you can comfortably and quickly get trusted TLS certificates that just @@ -27,6 +27,8 @@ All you need to do is: .. 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 ---------- @@ -37,8 +39,8 @@ This is a **DEVELOPER PREVIEW** intended for developers and testers only. SIGNED BY A TEST CA, AND WILL CAUSE CERT WARNINGS FOR USERS.** -Features -======== +Current Features +---------------- * web servers supported: @@ -53,7 +55,7 @@ Features * optionally can install a http->https redirect, so your site effectively runs https only * fully automated -* configuration changes can be rolled back N checkpoints +* configuration changes are logged and can be reverted using the CLI * text and ncurses UI * Free and Open Source Software, made with Python. @@ -70,9 +72,7 @@ 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) +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/index.rst b/docs/index.rst index 0387269ab..b290b2231 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,6 +8,11 @@ Welcome to the Let's Encrypt client documentation! using project +.. toctree:: + :maxdepth: 1 + + api + Indices and tables ================== diff --git a/docs/project.rst b/docs/project.rst index 57010b8e2..fa59c1af3 100644 --- a/docs/project.rst +++ b/docs/project.rst @@ -2,6 +2,8 @@ The Let's Encrypt Client Project ================================ +.. _hacking: + Hacking ======= @@ -12,47 +14,52 @@ 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 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 +- ``./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 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. +- ``./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 ============ -Most importantly, **be consistent with the rest of the code**, please. +Please: -1. Read [PEP 8 - Style Guide for Python Code] -(https://www.python.org/dev/peps/pep-0008). +1. **Be consistent with the rest of the code**. -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: +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. + def foo(arg): + """Short description. - :returns: Argument - :rtype: int + :param int arg: Some number. - """ - return arg + :returns: Argument + :rtype: int -3. Remember to use `./venv/bin/pylint`. + """ + 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 @@ -67,12 +74,4 @@ In order to generate the Sphinx documentation, run the following commands. make clean html SPHINXBUILD=../venv/bin/sphinx-build -This should generate documentation in the `docs/_build/html` directory. - -API documentation -================= - -.. toctree:: - :glob: - - api/** +This should generate documentation in the ``docs/_build/html`` directory. diff --git a/docs/using.rst b/docs/using.rst index cda2d7ec4..441bf1623 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -6,16 +6,17 @@ Prerequisites ============= 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)). +closely related `Debian is known to fail`_). Therefore, prerequisites for other platforms listed below are provided -mainly for the [developers](#hacking) reference. +mainly for the :ref:`developers ` reference. In general: * `swig`_ is required for compiling `m2crypto`_ -* `augeas`_ is required for the `python-augeas` bindings +* `augeas`_ is required for the ``python-augeas`` bindings + +.. _Debian is known to fail: https://github.com/letsencrypt/lets-encrypt-preview/issues/68 Ubuntu ------ @@ -30,6 +31,7 @@ Mac OSX ------- :: + sudo brew install augeas swig From 183106252962184c92e6c851fe4e8368fdf4095a Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 26 Jan 2015 14:21:37 -0800 Subject: [PATCH 08/35] cleanup errors.py use in client_test, small changes to display doc --- letsencrypt/client/display.py | 9 ++++++++- letsencrypt/client/tests/client_test.py | 11 +++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index fa8b43ce3..aad1d2be8 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -61,7 +61,7 @@ class NcursesDisplay(CommonDisplayMixin): """Display a menu. :param str message: title of menu - :param choices: Menu lines + :param choices: menu lines :type choices: list of tuples (tag, item) or list of items (tags will be enumerated) @@ -392,10 +392,16 @@ class FileDisplay(CommonDisplayMixin): self.outfile.write("\nCertificate Information:\n") self.outfile.write(cert_info_frame(cert)) + # Display exit codes OK = "ok" +"""Display exit code indicating user acceptance""" + CANCEL = "cancel" +"""Display exit code for a user canceling the display""" + HELP = "help" +"""Display exit code when for when the user requests more help.""" def cert_info_frame(cert): @@ -425,6 +431,7 @@ def gen_https_names(domains): """Returns a string of the https domains. Domains are formatted nicely with https:// prepended to each. + .. todo:: This should not use +=, rewrite this with unittests """ result = "" diff --git a/letsencrypt/client/tests/client_test.py b/letsencrypt/client/tests/client_test.py index 8e29d53c8..3c1096b34 100644 --- a/letsencrypt/client/tests/client_test.py +++ b/letsencrypt/client/tests/client_test.py @@ -3,6 +3,8 @@ import unittest import mock +from letsencrypt.client import errors + class RollbackTest(unittest.TestCase): """Test the rollback function.""" @@ -27,8 +29,7 @@ class RollbackTest(unittest.TestCase): @mock.patch("letsencrypt.client.reverter.Reverter") @mock.patch("letsencrypt.client.client.determine_installer") def test_misconfiguration_fixed(self, mock_det, mock_rev, mock_input): - from letsencrypt.client.errors import LetsEncryptMisconfigurationError - mock_det.side_effect = [LetsEncryptMisconfigurationError, + mock_det.side_effect = [errors.LetsEncryptMisconfigurationError, self.m_install] mock_input().generic_yesno.return_value = True @@ -47,8 +48,7 @@ class RollbackTest(unittest.TestCase): @mock.patch("letsencrypt.client.client.determine_installer") def test_misconfiguration_remains( self, mock_det, mock_rev, mock_warn, mock_input): - from letsencrypt.client.errors import LetsEncryptMisconfigurationError - mock_det.side_effect = LetsEncryptMisconfigurationError + mock_det.side_effect = errors.LetsEncryptMisconfigurationError mock_input().generic_yesno.return_value = True @@ -68,8 +68,7 @@ class RollbackTest(unittest.TestCase): @mock.patch("letsencrypt.client.client.determine_installer") def test_user_decides_to_manually_investigate( self, mock_det, mock_rev, mock_input): - from letsencrypt.client.errors import LetsEncryptMisconfigurationError - mock_det.side_effect = LetsEncryptMisconfigurationError + mock_det.side_effect = errors.LetsEncryptMisconfigurationError mock_input().generic_yesno.return_value = False From eeb58d92323172363b0eea10ffa1110b5df6c971 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 26 Jan 2015 14:24:45 -0800 Subject: [PATCH 09/35] pep8 fix display --- letsencrypt/client/display.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index aad1d2be8..74d0c2c69 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -310,7 +310,6 @@ class FileDisplay(CommonDisplayMixin): "enabled %s!" % gen_https_names(domains)) self.outfile.write("%s\n%s\n%s\n" % (side_frame, msg, side_frame)) - def display_certs(self, certs): """Display certificates for revocation. From 03a5750d90c1517f0bfde601ac1c986a92d27906 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 26 Jan 2015 22:25:08 -0800 Subject: [PATCH 10/35] modify gen_dvsni_cert api --- letsencrypt/client/apache/dvsni.py | 21 +++++-- letsencrypt/client/challenge_util.py | 15 ++--- letsencrypt/client/tests/apache/dvsni_test.py | 63 +++++++++++++------ .../client/tests/challenge_util_test.py | 44 +++++-------- 4 files changed, 80 insertions(+), 63 deletions(-) diff --git a/letsencrypt/client/apache/dvsni.py b/letsencrypt/client/apache/dvsni.py index cf5c3bdb0..d514dbbdb 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/challenge_util.py b/letsencrypt/client/challenge_util.py index b5198217d..b5d1cf38d 100644 --- a/letsencrypt/client/challenge_util.py +++ b/letsencrypt/client/challenge_util.py @@ -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.client.Client.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/tests/apache/dvsni_test.py b/letsencrypt/client/tests/apache/dvsni_test.py index 68ffa283b..53e7f81e3 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])): 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,26 +106,23 @@ 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( diff --git a/letsencrypt/client/tests/challenge_util_test.py b/letsencrypt/client/tests/challenge_util_test.py index 759ee34ce..84a561d5d 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 client @@ -19,32 +18,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 = client.Client.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.""" @@ -60,7 +46,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) From 624e59d3818a57f2b0d24fa7ab9a30cc166aea7c Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 27 Jan 2015 14:41:23 -0800 Subject: [PATCH 11/35] update intro paragraph of README --- README.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index c66fcabf6..277dbe9de 100644 --- a/README.rst +++ b/README.rst @@ -3,9 +3,10 @@ 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 that talks to the Let's Encrypt CA -so you can comfortably and quickly get trusted TLS certificates that just -work without warnings in every browser. +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: From d43b1dbf92852b152c7fdc1fd553ed4c431627f3 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 27 Jan 2015 14:53:28 -0800 Subject: [PATCH 12/35] range->xrange --- letsencrypt/client/tests/apache/dvsni_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/tests/apache/dvsni_test.py b/letsencrypt/client/tests/apache/dvsni_test.py index 53e7f81e3..a50f0a3f6 100644 --- a/letsencrypt/client/tests/apache/dvsni_test.py +++ b/letsencrypt/client/tests/apache/dvsni_test.py @@ -83,8 +83,8 @@ class DvsniPerformTest(util.ApacheTest): (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): @@ -129,7 +129,7 @@ class DvsniPerformTest(util.ApacheTest): "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): From 0a44bbb7a128a8a069424647b16ff0b5829f5585 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Jan 2015 12:56:12 +0000 Subject: [PATCH 13/35] Simpler Travis CI matrix --- .travis.yml | 13 ++----------- setup.py | 3 +-- tox.ini | 10 ++++++---- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index b23cfe540..7f625f663 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,5 @@ -# To mimic README.rst 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/setup.py b/setup.py index 4f192b886..7033dfd70 100755 --- a/setup.py +++ b/setup.py @@ -25,8 +25,7 @@ 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', 'tox', ] diff --git a/tox.ini b/tox.ini index 190bfd2d7..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=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 From 79bb3cc80dca4922a12568946520927bfe4e5511 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Jan 2015 13:02:14 +0000 Subject: [PATCH 14/35] pylint: upstream fixed #248 in 1.4.0 --- .pylintrc | 6 +----- letsencrypt/client/apache/configurator.py | 2 +- letsencrypt/client/log.py | 2 +- letsencrypt/client/tests/acme_test.py | 2 +- letsencrypt/client/tests/apache/util.py | 2 +- letsencrypt/client/tests/crypto_util_test.py | 4 +++- letsencrypt/client/tests/reverter_test.py | 1 + setup.py | 2 +- 8 files changed, 10 insertions(+), 11 deletions(-) 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/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/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/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/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/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 e7766e331..39ef3d135 100644 --- a/letsencrypt/client/tests/reverter_test.py +++ b/letsencrypt/client/tests/reverter_test.py @@ -385,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/setup.py b/setup.py index 7033dfd70..004388b5e 100755 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ testing_extras = [ 'coverage', 'nose', 'nosexcover', - 'pylint', + 'pylint>=1.4.0', # upstream #248 'tox', ] From e9a6d6039bb2a3acc53175a829a262f2c31edf17 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 28 Jan 2015 16:18:40 -0800 Subject: [PATCH 15/35] Remove half-implemented code... leave TODO --- letsencrypt/client/client.py | 50 ------------------------------------ letsencrypt/scripts/main.py | 7 +++-- 2 files changed, 5 insertions(+), 52 deletions(-) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 0f5166399..223a1ce3a 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -4,8 +4,6 @@ import csv import logging import os import shutil -import socket -import string import sys import M2Crypto @@ -25,11 +23,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. @@ -96,8 +89,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( @@ -401,47 +392,6 @@ def csr_pem_to_der(csr): return Client.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/scripts/main.py b/letsencrypt/scripts/main.py index 4dfa70764..d9658238e 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 ect... + +""" import argparse import logging import os @@ -165,7 +169,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") From a5f65dcfd820167ed7d79905b564a5dd3c85df04 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 28 Jan 2015 16:20:09 -0800 Subject: [PATCH 16/35] pep8 - remove extra whitespace --- letsencrypt/scripts/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index d9658238e..2997d9188 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -180,7 +180,6 @@ def get_all_names(installer): return names - def read_file(filename): """Returns the given file's contents with universal new line support. From 9c98e1e7e5608b2f6f0fcdf345d926fc306cba94 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Jan 2015 01:20:45 +0100 Subject: [PATCH 17/35] have letsencrypt.VERSION, show it in letsencrypt --help, use it in setup.py note: we had some discussion about potential problems importing VERSION from main package. SO link: http://stackoverflow.com/questions/2058802/how-can-i-get-the-version-defined-in-setup-py-setuptools-in-my-package See also my comment in __init__.py - maybe we can add that "version detection from git tags" magic later. --- letsencrypt/__init__.py | 9 +++++++++ letsencrypt/scripts/main.py | 3 ++- setup.py | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/letsencrypt/__init__.py b/letsencrypt/__init__.py index 9c0ff7662..0f56daa29 100644 --- a/letsencrypt/__init__.py +++ b/letsencrypt/__init__.py @@ -1 +1,10 @@ """Let's Encrypt.""" + +# do not import stuff here. this file is used by setup.py, thus importing +# stuff here might break setup.py as dependencies are not installed yet. + +VERSION_TUPLE = 0, 1, 0, "a0" +"""version tuple: major, minor, micro, {a|b|rc}N - see PEP440""" + +VERSION = "%d.%d.%d%s" % VERSION_TUPLE +"""version as str""" diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 4dfa70764..c30b455c3 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -8,6 +8,7 @@ import sys import zope.component import zope.interface +from letsencrypt import VERSION from letsencrypt.client import CONFIG from letsencrypt.client import client from letsencrypt.client import display @@ -19,7 +20,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" % VERSION) parser.add_argument("-d", "--domains", dest="domains", metavar="DOMAIN", nargs="+") diff --git a/setup.py b/setup.py index 004388b5e..5737e83c3 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/env python from setuptools import setup +from letsencrypt import VERSION install_requires = [ 'argparse', @@ -31,7 +32,7 @@ testing_extras = [ setup( name="letsencrypt", - version="0.1", + version=VERSION, description="Let's Encrypt", author="Let's Encrypt Project", license="", From f6f6b792215fa3e6991e080a9f3a2a3cdf940538 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Thu, 29 Jan 2015 00:17:45 -0800 Subject: [PATCH 18/35] fix comment spelling --- letsencrypt/scripts/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 2997d9188..20d3022f6 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """Parse command line and call the appropriate functions. -..todo:: Sanity check all input. Be sure to avoid shell code ect... +..todo:: Sanity check all input. Be sure to avoid shell code etc... """ import argparse From 219b25cd9878144ca2b4a6c65d55d4bec72c822b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Jan 2015 14:58:20 +0100 Subject: [PATCH 19/35] parse version number from package init, avoid package import --- letsencrypt/__init__.py | 9 +-------- letsencrypt/scripts/main.py | 4 ++-- setup.py | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/letsencrypt/__init__.py b/letsencrypt/__init__.py index 0f56daa29..11f2f3f01 100644 --- a/letsencrypt/__init__.py +++ b/letsencrypt/__init__.py @@ -1,10 +1,3 @@ """Let's Encrypt.""" -# do not import stuff here. this file is used by setup.py, thus importing -# stuff here might break setup.py as dependencies are not installed yet. - -VERSION_TUPLE = 0, 1, 0, "a0" -"""version tuple: major, minor, micro, {a|b|rc}N - see PEP440""" - -VERSION = "%d.%d.%d%s" % VERSION_TUPLE -"""version as str""" +__version__ = "0.1" diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index c30b455c3..81677058e 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -8,7 +8,7 @@ import sys import zope.component import zope.interface -from letsencrypt import VERSION +import letsencrypt from letsencrypt.client import CONFIG from letsencrypt.client import client from letsencrypt.client import display @@ -20,7 +20,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="letsencrypt client %s" % VERSION) + description="letsencrypt client %s" % letsencrypt.__version__) parser.add_argument("-d", "--domains", dest="domains", metavar="DOMAIN", nargs="+") diff --git a/setup.py b/setup.py index 5737e83c3..a6038db56 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,17 @@ #!/usr/bin/env python +import os +import re +import codecs + from setuptools import setup -from letsencrypt import VERSION +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 meta_file: + content = meta_file.read() +meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", content)) install_requires = [ 'argparse', @@ -32,7 +42,7 @@ testing_extras = [ setup( name="letsencrypt", - version=VERSION, + version=meta['version'], description="Let's Encrypt", author="Let's Encrypt Project", license="", From af8edbc21c0d637ab80d0b8a9b7208be585feba2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Jan 2015 15:25:28 +0100 Subject: [PATCH 20/35] put file reading into function, we'll soon need that for more stuff like e.g. reading long_description from README.rst + CHANGES.rst. --- letsencrypt/__init__.py | 1 - setup.py | 10 +++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/letsencrypt/__init__.py b/letsencrypt/__init__.py index 11f2f3f01..9fe93c4db 100644 --- a/letsencrypt/__init__.py +++ b/letsencrypt/__init__.py @@ -1,3 +1,2 @@ """Let's Encrypt.""" - __version__ = "0.1" diff --git a/setup.py b/setup.py index a6038db56..6c30593c0 100755 --- a/setup.py +++ b/setup.py @@ -5,13 +5,17 @@ import codecs 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') -with codecs.open(init_fn, encoding='utf8') as meta_file: - content = meta_file.read() -meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", content)) +meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", read_file(init_fn))) install_requires = [ 'argparse', From 47e49215c3f896199e8e9300d6177308a1be5e3b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Jan 2015 15:38:20 +0100 Subject: [PATCH 21/35] long_description = README.rst (+ CHANGES.rst later) --- MANIFEST.in | 1 + setup.py | 5 +++++ 2 files changed, 6 insertions(+) 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/setup.py b/setup.py index 6c30593c0..0cdbdefe4 100755 --- a/setup.py +++ b/setup.py @@ -11,12 +11,16 @@ def read_file(filename, encoding='utf8'): 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', @@ -48,6 +52,7 @@ setup( name="letsencrypt", version=meta['version'], description="Let's Encrypt", + long_description=readme, # later: + '\n\n' + changes author="Let's Encrypt Project", license="", url="https://letsencrypt.org", From 4c7b2d202c1f98a7ee43f3fc19a16e95d8f3214b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Jan 2015 23:28:22 +0100 Subject: [PATCH 22/35] cosmetic fixes --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0cdbdefe4..a2a4fd9e9 100755 --- a/setup.py +++ b/setup.py @@ -1,13 +1,13 @@ #!/usr/bin/env python +import codecs import os import re -import codecs from setuptools import setup def read_file(filename, encoding='utf8'): - """read unicode from given file""" + """Read unicode from given file.""" with codecs.open(filename, encoding=encoding) as fd: return fd.read() From bd8b908f5011c572026a64283387952f3e90737c Mon Sep 17 00:00:00 2001 From: James Kasten Date: Thu, 29 Jan 2015 19:35:31 -0800 Subject: [PATCH 23/35] patch auth_handler cleanup function --- letsencrypt/client/auth_handler.py | 28 +++++++++++++++++++++++----- letsencrypt/client/errors.py | 14 +++++++++++--- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index b85996818..7ede54c94 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,10 @@ 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 + 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. 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.""" From 2cb4ab936d00e16aae143e199e9308d9cb94655c Mon Sep 17 00:00:00 2001 From: James Kasten Date: Thu, 29 Jan 2015 20:07:29 -0800 Subject: [PATCH 24/35] generator to list to conform to API --- letsencrypt/client/auth_handler.py | 6 +- letsencrypt/client/tests/auth_handler_test.py | 84 ++++++++++++++++--- 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index 7ede54c94..7c72980ed 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -208,9 +208,11 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes """ logging.info("Cleaning up challenges for %s", domain) # These are indexed challenges... give just the challenges to the auth - self.dv_auth.cleanup(ichall.chall for ichall in self.dv_c[domain]) + # 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]) + [ichall.chall for ichall in self.client_c[domain]]) def _cleanup_state(self, delete_list): """Cleanup state after an authorization is received. diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index b80c3c61d..33dfb6907 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -1,4 +1,5 @@ """Tests for letsencrypt.client.auth_handler.""" +import logging import unittest import mock @@ -35,6 +36,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 +60,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,7 +73,7 @@ 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) @@ -140,7 +146,7 @@ class SatisfyChallengesTest(unittest.TestCase): 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 +159,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) @@ -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) @@ -229,6 +235,49 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual( type(self.handler.client_c["4"][0].chall).__name__, "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") + + path = gen_path(["dvsni", "proofOfPossession"], challenges) + mock_chall_path.return_value = path + + # 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.assertEqual(type(arg_chall_list[0]).__name__, "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.assertEqual(type(arg_chall_list[0]).__name__, "PopChall") + + def _get_exp_response(self, domain, path, challenges): # pylint: disable=no-self-use exp_resp = ["null"] * len(challenges) for i in path: @@ -259,7 +308,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 +326,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 +363,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 +437,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 +454,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: list of str, challenge message types + :param dict challenges: ACME challenge messages + + :return: list of int + + """ path = [] for i, chall in enumerate(challenges): for str_chall in str_list: From 94c3eb0533c7bb4e88cf6176e0d6417f1163ce66 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 30 Jan 2015 12:03:55 -0800 Subject: [PATCH 25/35] remove old code --- letsencrypt/client/interactive_challenge.py | 42 ------- .../client/recovery_contact_challenge.py | 118 ------------------ 2 files changed, 160 deletions(-) delete mode 100644 letsencrypt/client/interactive_challenge.py delete mode 100644 letsencrypt/client/recovery_contact_challenge.py 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/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, - } From e6a88c4e27f7b891268922a858dd92679a8247ea Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 30 Jan 2015 21:20:40 +0000 Subject: [PATCH 26/35] interfaces: disable inherit-non-class pylint msg --- letsencrypt/client/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index 9e35a754a..d2354eb6d 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): From 29531bf414159cb07f21c88995ce247c097b4a15 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 30 Jan 2015 15:49:31 -0800 Subject: [PATCH 27/35] remove setup.sh --- letsencrypt/client/setup.sh | 2 -- 1 file changed, 2 deletions(-) delete mode 100755 letsencrypt/client/setup.sh 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 From 1725829477fec71b957016bab40000f2e6465cd8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Jan 2015 00:28:09 +0000 Subject: [PATCH 28/35] Autodoc interfaces. Spelling. --- docs/conf.py | 1 + letsencrypt/client/interfaces.py | 27 +++++++++++++++++++-------- setup.py | 1 + 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index fd089d14b..fbcd61065 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,6 +34,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. diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index 9e35a754a..7a7fa1593 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -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,6 +23,7 @@ class IAuthenticator(zope.interface.Interface): :rtype: list """ + def perform(chall_list): """Perform the given challenge. @@ -35,6 +37,7 @@ class IAuthenticator(zope.interface.Interface): :rtype: `list` of dicts """ + def cleanup(chall_list): """Revert changes and shutdown after challenges complete.""" @@ -58,6 +61,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,8 +73,9 @@ 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): + """Peform a configuration enhancement. :param str domain: domain for which to provide enhancement :param str enhancement: An enhancement as defined in CONFIG.ENHANCEMENTS @@ -80,14 +85,16 @@ class IInstaller(zope.interface.Interface): for each enhancement. """ - def supported_enhancements(): - """Returns a list of supported enhancments. - :returns: supported enhancments which should be a subset of the - enhancments in :class:`letsencrypt.client.CONFIG.ENHANCEMENTS` - :rtype: `list` of `str` + def supported_enhancements(): + """Returns a list of supported enhancements. + + :returns: supported enhancements which should be a subset of the + enhancements in :class:`letsencrypt.client.CONFIG.ENHANCEMENTS` + :rtype: `list` of `str` """ + def get_all_certs_keys(): """Retrieve all certs and keys set in configuration. @@ -98,6 +105,7 @@ class IInstaller(zope.interface.Interface): :rtype: list """ + def save(title=None, temporary=False): """Saves all changes to the configuration files. @@ -113,6 +121,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,6 +144,7 @@ class IDisplay(zope.interface.Interface): :param str message: Message to display """ + def generic_menu(message, choices, input_text=""): """Displays a generic menu. @@ -143,6 +153,7 @@ class IDisplay(zope.interface.Interface): :param str input_text: instructions on how to make a selection """ + def generic_input(message): """Accept input from the user.""" @@ -168,7 +179,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): diff --git a/setup.py b/setup.py index a2a4fd9e9..5501c7dd6 100755 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ install_requires = [ ] docs_extras = [ + 'repoze.sphinx.autointerface', 'Sphinx', ] From f890dabb7e32307e90cc4636c84184c63817d141 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Jan 2015 01:23:42 +0000 Subject: [PATCH 29/35] Improve interfaces docstrings --- letsencrypt/client/interfaces.py | 51 +++++++++++++++++++------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index 7a7fa1593..8372afa15 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -28,13 +28,16 @@ class IAuthenticator(zope.interface.Interface): """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` """ @@ -75,33 +78,36 @@ class IInstaller(zope.interface.Interface): """ def enhance(domain, enhancement, options=None): - """Peform a configuration enhancement. + """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 enhancements. - :returns: supported enhancements which should be a subset of the - enhancements 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 """ @@ -149,7 +155,10 @@ class IDisplay(zope.interface.Interface): """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 """ @@ -189,7 +198,7 @@ class IValidator(zope.interface.Interface): """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.""" From 44c2b38cdef0a2f8bda14df36717bec2c4aae0cd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 31 Jan 2015 06:01:52 +0100 Subject: [PATCH 30/35] use version from package init also for sphinx docs, insert toplevel dir into sys.path --- docs/conf.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index fbcd61065..dce5e21d9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,13 +12,28 @@ # 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 + + +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))) # 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 ------------------------------------------------ @@ -58,9 +73,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. From f082aa3186a5f5ab40e1473e8cdb98abae1a7578 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Sat, 31 Jan 2015 02:20:15 -0800 Subject: [PATCH 31/35] Remove associated docs --- docs/api/client/interactive_challenge.rst | 5 ----- docs/api/client/recovery_contact_challenge.rst | 5 ----- 2 files changed, 10 deletions(-) delete mode 100644 docs/api/client/interactive_challenge.rst delete mode 100644 docs/api/client/recovery_contact_challenge.rst 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: From c42f512c2a3c099461979be406503e6aa29278aa Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 1 Feb 2015 03:15:39 +0100 Subject: [PATCH 32/35] refactor conf.py, add comment about version syntax --- docs/conf.py | 10 ++-------- letsencrypt/__init__.py | 1 + 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index dce5e21d9..018d2afed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,18 +17,12 @@ import os import re import sys - -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))) +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 diff --git a/letsencrypt/__init__.py b/letsencrypt/__init__.py index 9fe93c4db..b36747b5f 100644 --- a/letsencrypt/__init__.py +++ b/letsencrypt/__init__.py @@ -1,2 +1,3 @@ """Let's Encrypt.""" +# version number like 1.2.3a0, must have at least 2 parts, like 1.2 __version__ = "0.1" From 4d247fa6a157aa3a699b23733d498f87d352c4e5 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 2 Feb 2015 00:47:39 -0800 Subject: [PATCH 33/35] formatting fixes for testing code --- letsencrypt/client/tests/auth_handler_test.py | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index 33dfb6907..c1655ea05 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -4,6 +4,7 @@ import unittest import mock +from letsencrypt.client import challenge_util from letsencrypt.client import errors from letsencrypt.client.tests import acme_util @@ -79,8 +80,8 @@ class SatisfyChallengesTest(unittest.TestCase): 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): @@ -108,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): @@ -122,7 +123,7 @@ class SatisfyChallengesTest(unittest.TestCase): acme_util.get_chall_msg(dom, "nonce0", challenges, combos), "dummy_key") - path = gen_path(["simpleHttps", "recoveryToken"], challenges) + path =gen_path(["simpleHttps", "recoveryToken"], challenges) mock_chall_path.return_value = path self.handler._satisfy_challenges() # pylint: disable=protected-access @@ -137,10 +138,10 @@ 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): @@ -173,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): @@ -219,21 +219,21 @@ 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.assertEqual( - type(self.handler.client_c["2"][0].chall).__name__, "PopChall") - self.assertEqual( - type(self.handler.client_c["4"][0].chall).__name__, "RecTokenChall") + 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): @@ -251,8 +251,8 @@ class SatisfyChallengesTest(unittest.TestCase): str(i), "nonce%d" % i, challenges, combos), "dummy_key") - path = gen_path(["dvsni", "proofOfPossession"], challenges) - mock_chall_path.return_value = path + 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, @@ -268,14 +268,16 @@ class SatisfyChallengesTest(unittest.TestCase): # Assert length of arg list was 1 arg_chall_list = mock_cleanup_args[i][0][0] self.assertEqual(len(arg_chall_list), 1) - self.assertEqual(type(arg_chall_list[0]).__name__, "DvsniChall") + 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.assertEqual(type(arg_chall_list[0]).__name__, "PopChall") + self.assertTrue(isinstance(arg_chall_list[0], + challenge_util.PopChall)) def _get_exp_response(self, domain, path, challenges): # pylint: disable=no-self-use @@ -467,10 +469,10 @@ def gen_auth_resp(chall_list): def gen_path(str_list, challenges): """Generate a path for challenge messages - :param list str_list: list of str, challenge message types + :param list str_list: challenge message types (:class:`str`) :param dict challenges: ACME challenge messages - :return: list of int + :return: :class:`list` of :class:`int` """ path = [] From 687668ec7529b4c19adebd9c60b2ba81c7b9c0c3 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 2 Feb 2015 01:52:54 -0800 Subject: [PATCH 34/35] indentation pylint fixes --- letsencrypt/client/tests/auth_handler_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/letsencrypt/client/tests/auth_handler_test.py b/letsencrypt/client/tests/auth_handler_test.py index c1655ea05..9c3effe1b 100644 --- a/letsencrypt/client/tests/auth_handler_test.py +++ b/letsencrypt/client/tests/auth_handler_test.py @@ -123,7 +123,7 @@ class SatisfyChallengesTest(unittest.TestCase): acme_util.get_chall_msg(dom, "nonce0", challenges, combos), "dummy_key") - path =gen_path(["simpleHttps", "recoveryToken"], challenges) + path = gen_path(["simpleHttps", "recoveryToken"], challenges) mock_chall_path.return_value = path self.handler._satisfy_challenges() # pylint: disable=protected-access @@ -141,7 +141,7 @@ class SatisfyChallengesTest(unittest.TestCase): 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)) + challenge_util.RecTokenChall)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") def test_name5_all(self, mock_chall_path): @@ -175,9 +175,9 @@ class SatisfyChallengesTest(unittest.TestCase): self.assertEqual(len(self.handler.client_c[dom]), 1) self.assertTrue(isinstance(self.handler.dv_c[dom][0].chall, - challenge_util.DvsniChall)) + challenge_util.DvsniChall)) self.assertTrue(isinstance(self.handler.client_c[dom][0].chall, - challenge_util.RecContactChall)) + challenge_util.RecContactChall)) @mock.patch("letsencrypt.client.auth_handler.gen_challenge_path") def test_name5_mix(self, mock_chall_path): @@ -220,7 +220,7 @@ class SatisfyChallengesTest(unittest.TestCase): len(self.handler.client_c[dom]), len(chosen_chall[i]) - 1) self.assertTrue(isinstance(self.handler.dv_c["0"][0].chall, - challenge_util.DnsChall)) + 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, From 860a9a77b01a370ae2554cd3ad87bde6b3da124d Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 2 Feb 2015 02:02:11 -0800 Subject: [PATCH 35/35] Fix DnsChall --- letsencrypt/client/auth_handler.py | 3 +-- letsencrypt/client/challenge_util.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/auth_handler.py b/letsencrypt/client/auth_handler.py index 7c72980ed..f53c2f1cc 100644 --- a/letsencrypt/client/auth_handler.py +++ b/letsencrypt/client/auth_handler.py @@ -299,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 b5d1cf38d..6c70263d7 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(