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:
James Kasten 2015-02-02 19:02:38 -08:00
commit e000cfd7c6
38 changed files with 669 additions and 623 deletions

View file

@ -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]

View file

@ -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
View 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.

View file

@ -1,3 +1,4 @@
include README.rst CHANGES.rst
recursive-include letsencrypt *.json
recursive-include letsencrypt *.sh
recursive-include letsencrypt *.conf

143
README.md
View file

@ -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
View 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
View file

@ -0,0 +1,8 @@
=================
API Documentation
=================
.. toctree::
:glob:
api/**

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt.client.interactive_challenge`
-----------------------------------------------
.. automodule:: letsencrypt.client.interactive_challenge
:members:

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt.client.recovery_contact_challenge`
----------------------------------------------------
.. automodule:: letsencrypt.client.recovery_contact_challenge
:members:

View file

@ -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.

View file

@ -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
View file

@ -0,0 +1,6 @@
============
Introduction
============
.. include:: ../README.rst
.. include:: ../CHANGES.rst

77
docs/project.rst Normal file
View 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
View 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/

View file

@ -1 +1,3 @@
"""Let's Encrypt."""
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
__version__ = "0.1"

View file

@ -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

View file

@ -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.

View file

@ -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(

View file

@ -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):

View file

@ -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."""

View file

@ -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."""

View file

@ -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

View file

@ -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."""

View file

@ -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).

View file

@ -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,
}

View file

@ -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

View file

@ -1,2 +0,0 @@
#!/bin/sh
cp options-ssl.conf /etc/letsencrypt/options-ssl.conf

View file

@ -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

View file

@ -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):

View file

@ -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()

View file

@ -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:

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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.

View file

@ -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
View file

@ -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