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