Merge remote-tracking branch 'upstream/master' into bugfix_redirect

This commit is contained in:
sagi
2015-12-03 22:12:49 +00:00
32 changed files with 631 additions and 369 deletions
-1
View File
@@ -1 +0,0 @@
letsencrypt/DISCLAIMER
+1 -1
View File
@@ -33,7 +33,7 @@ RUN /opt/letsencrypt/src/ubuntu.sh && \
# Dockerfile we make sure we cache as much as possible
# py26reqs.txt not installed!
COPY setup.py README.rst CHANGES.rst MANIFEST.in DISCLAIMER linter_plugin.py tox.cover.sh tox.ini pep8.travis.sh .pep8 .pylintrc /opt/letsencrypt/src/
COPY setup.py README.rst CHANGES.rst MANIFEST.in linter_plugin.py tox.cover.sh tox.ini pep8.travis.sh .pep8 .pylintrc /opt/letsencrypt/src/
# all above files are necessary for setup.py, however, package source
# code directory has to be copied separately to a subdirectory...
-1
View File
@@ -4,7 +4,6 @@ include CHANGES.rst
include CONTRIBUTING.md
include LICENSE.txt
include linter_plugin.py
include letsencrypt/DISCLAIMER
recursive-include docs *
recursive-include examples *
recursive-include letsencrypt/tests/testdata *
+108 -66
View File
@@ -3,12 +3,9 @@
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.**
Browser-trusted certificates will be available in the coming months.
The Let's Encrypt Client is **BETA SOFTWARE**. It contains plenty of bugs and
rough edges, and should be tested thoroughly in staging environments before use
on production systems.
For more information regarding the status of the project, please see
https://letsencrypt.org. Be sure to checkout the
@@ -17,38 +14,87 @@ https://letsencrypt.org. Be sure to checkout the
About the Let's Encrypt Client
==============================
The Let's Encrypt Client is a fully-featured, extensible client for the Let's
Encrypt CA (or any other CA that speaks the `ACME
<https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md>`_
protocol) that can automate the tasks of obtaining certificates and
configuring webservers to use them.
Installation
------------
If ``letsencrypt`` is packaged for your OS, you can install it from there, and
run it by typing ``letsencrypt``. Because not all operating systems have
packages yet, we provide a temporary solution via the ``letsencrypt-auto``
wrapper script, which obtains some dependencies from your OS and puts others
in an python virtual environment::
user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt
user@webserver:~$ cd letsencrypt
user@webserver:~/letsencrypt$ ./letsencrypt-auto --help
Or for full command line help, type::
./letsencrypt-auto --help all
``letsencrypt-auto`` updates to the latest client release automatically. And
since ``letsencrypt-auto`` is a wrapper to ``letsencrypt``, it accepts exactly
the same command line flags and arguments. More details about this script and
other installation methods can be found `in the User Guide
<https://letsencrypt.readthedocs.org/en/latest/using.html#installation>`_.
How to run the client
---------------------
In many cases, you can just run ``letsencrypt-auto`` or ``letsencrypt``, and the
client will guide you through the process of obtaining and installing certs
interactively.
You can also tell it exactly what you want it to do from the command line.
For instance, if you want to obtain a cert for ``thing.com``,
``www.thing.com``, and ``otherthing.net``, using the Apache plugin to both
obtain and install the certs, you could do this::
./letsencrypt-auto --apache -d thing.com -d www.thing.com -d otherthing.net
(The first time you run the command, it will make an account, and ask for an
email and agreement to the Let's Encrypt Subscriber Agreement; you can
automate those with ``--email`` and ``--agree-tos``)
If you want to use a webserver that doesn't have full plugin support yet, you
can still use "standalone" or "webroot" plugins to obtain a certificate::
./letsencrypt-auto certonly --standalone --email admin@thing.com -d thing.com -d www.thing.com -d otherthing.net
Understanding the client in more depth
--------------------------------------
To understand what the client is doing in detail, it's important to
understand the way it uses plugins. Please see the `explanation of
plugins <https://letsencrypt.readthedocs.org/en/latest/using.html#plugins>`_ in
the User Guide.
Links
=====
Documentation: https://letsencrypt.readthedocs.org
Software project: https://github.com/letsencrypt/letsencrypt
Notes for developers: https://letsencrypt.readthedocs.org/en/latest/contributing.html
Main Website: https://letsencrypt.org/
IRC Channel: #letsencrypt on `Freenode`_
Community: https://community.letsencrypt.org
Mailing list: `client-dev`_ (to subscribe without a Google account, send an
email to client-dev+subscribe@letsencrypt.org)
|build-status| |coverage| |docs| |container|
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.
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 to sign a single domain is::
user@www:~$ sudo letsencrypt -d www.example.org certonly
For multiple domains (SAN) use::
user@www:~$ sudo letsencrypt -d www.example.org -d example.org certonly
and if you have a compatible web server (Apache or Nginx), Let's Encrypt can
not only get a new certificate, but also deploy it and configure your
server automatically!::
user@www:~$ sudo letsencrypt -d www.example.org run
**Encrypt ALL the things!**
.. |build-status| image:: https://travis-ci.org/letsencrypt/letsencrypt.svg?branch=master
@@ -72,18 +118,38 @@ server automatically!::
.. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU
System Requirements
===================
The Let's Encrypt Client presently only runs on Unix-ish OSes that include
Python 2.6 or 2.7; Python 3.x support will be added after the Public Beta
launch. The client requires root access in order to write to
``/etc/letsencrypt``, ``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to
bind to ports 80 and 443 (if you use the ``standalone`` plugin) and to read and
modify webserver configurations (if you use the ``apache`` or ``nginx``
plugins). If none of these apply to you, it is theoretically possible to run
without root privilegess, but for most users who want to avoid running an ACME
client as root, either `letsencrypt-nosudo
<https://github.com/diafygi/letsencrypt-nosudo>`_ or `simp_le
<https://github.com/kuba/simp_le>`_ are more appropriate choices.
The Apache plugin currently requires a Debian-based OS with augeas version
1.0; this includes Ubuntu 12.04+ and Debian 7+.
Current Features
----------------
================
* Supports multiple web servers:
- apache/2.x (tested and working on Ubuntu Linux)
- nginx/0.8.48+ (under development)
- apache/2.x (working on Debian 8+ and Ubuntu 12.04+)
- standalone (runs its own simple webserver to prove you control a domain)
- webroot (adds files to webroot directories in order to prove control of
domains and obtain certs)
- nginx/0.8.48+ (highly experimental, not included in letsencrypt-auto)
* The private key is generated locally on your system.
* Can talk to the Let's Encrypt (demo) CA or optionally to other ACME
* Can talk to the Let's Encrypt CA or optionally to other ACME
compliant services.
* Can get domain-validated (DV) certificates.
* Can revoke certificates.
@@ -92,34 +158,10 @@ Current Features
runs https only (Apache only)
* Fully automated.
* Configuration changes are logged and can be reverted.
* Text and ncurses UI.
* Supports ncurses and text (-t) UI, or can be driven entirely from the
command line.
* Free and Open Source Software, made with Python.
Installation Instructions
-------------------------
Official **documentation**, including `installation instructions`_, is
available at https://letsencrypt.readthedocs.org.
Links
-----
Documentation: https://letsencrypt.readthedocs.org
Software project: https://github.com/letsencrypt/letsencrypt
Notes for developers: https://letsencrypt.readthedocs.org/en/latest/contributing.html
Main Website: https://letsencrypt.org/
IRC Channel: #letsencrypt on `Freenode`_
Community: https://community.letsencrypt.org
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
+1 -1
View File
@@ -194,7 +194,7 @@ class JSONDeSerializable(object):
:rtype: str
"""
return self.json_dumps(sort_keys=True, indent=4)
return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': '))
@classmethod
def json_dump_default(cls, python_object):
+1 -4
View File
@@ -1,8 +1,6 @@
"""Tests for acme.jose.interfaces."""
import unittest
import six
class JSONDeSerializableTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
@@ -92,9 +90,8 @@ class JSONDeSerializableTest(unittest.TestCase):
self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps())
def test_json_dumps_pretty(self):
filler = ' ' if six.PY2 else ''
self.assertEqual(self.seq.json_dumps_pretty(),
'[\n "foo1",{0}\n "foo2"\n]'.format(filler))
'[\n "foo1",\n "foo2"\n]')
def test_json_dump_default(self):
from acme.jose.interfaces import JSONDeSerializable
+1 -1
View File
@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.1.0.dev0'
version = '0.2.0.dev0'
install_requires = [
# load_pem_private/public_key (>=0.6)
+6 -4
View File
@@ -4,12 +4,13 @@
# - Fedora 22, 23 (x64)
# - Centos 7 (x64: onD igitalOcean droplet)
if type yum 2>/dev/null
then
tool=yum
elif type dnf 2>/dev/null
if type dnf 2>/dev/null
then
tool=dnf
elif type yum 2>/dev/null
then
tool=yum
else
echo "Neither yum nor dnf found. Aborting bootstrap!"
exit 1
@@ -40,6 +41,7 @@ if ! $tool install -y \
augeas-libs \
openssl-devel \
libffi-devel \
redhat-rpm-config \
ca-certificates
then
echo "Could not install additional dependencies. Aborting bootstrap!"
+144 -98
View File
@@ -28,13 +28,8 @@ Firstly, please `install Git`_ and run the following commands:
git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt
.. warning:: Alternatively you could `download the ZIP archive`_ and
extract the snapshot of our repository, but it's strongly
recommended to use the above method instead.
.. _`install Git`: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
.. _`download the ZIP archive`:
https://github.com/letsencrypt/letsencrypt/archive/master.zip
To install and run the client you just need to type:
@@ -61,107 +56,48 @@ or for full help, type:
./letsencrypt-auto --help all
Running with Docker
-------------------
Docker_ is an amazingly simple and quick way to obtain a
certificate. However, this mode of operation is unable to install
certificates or configure your webserver, because our installer
plugins cannot reach it from inside the Docker container.
You should definitely read the :ref:`where-certs` section, in order to
know how to manage the certs
manually. https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance
provides some information about recommended ciphersuites. If none of
these make much sense to you, you should definitely use the
letsencrypt-auto_ method, which enables you to use installer plugins
that cover both of those hard topics.
If you're still not convinced and have decided to use this method,
from the server that the domain you're requesting a cert for resolves
to, `install Docker`_, then issue the following command:
.. code-block:: shell
sudo docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \
-v "/etc/letsencrypt:/etc/letsencrypt" \
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
quay.io/letsencrypt/letsencrypt:latest auth
and follow the instructions (note that ``auth`` command is explicitly
used - no installer plugins involved). Your new cert will be available
in ``/etc/letsencrypt/live`` on the host.
.. _Docker: https://docker.com
.. _`install Docker`: https://docs.docker.com/userguide/
Operating System Packages
--------------------------
**FreeBSD**
* Port: ``cd /usr/ports/security/py-letsencrypt && make install clean``
* Package: ``pkg install py27-letsencrypt``
**Arch Linux**
.. code-block:: shell
sudo pacman -S letsencrypt letsencrypt-apache
**Other Operating Systems**
Unfortunately, this is an ongoing effort. If you'd like to package
Let's Encrypt client for your distribution of choice please have a
look at the :doc:`packaging`.
From source
-----------
Installation from source is only supported for developers and the
whole process is described in the :doc:`contributing`.
.. warning:: Please do **not** use ``python setup.py install`` or
``python pip install .``. Please do **not** attempt the
installation commands as superuser/root and/or without virtual
environment, e.g. ``sudo python setup.py install``, ``sudo pip
install``, ``sudo ./venv/bin/...``. These modes of operation might
corrupt your operating system and are **not supported** by the
Let's Encrypt team!
Comparison of different methods
-------------------------------
Unless you have a very specific requirements, we kindly ask you to use
the letsencrypt-auto_ method. It's the fastest, the most thoroughly
tested and the most reliable way of getting our software and the free
SSL certificates!
``letsencrypt-auto`` is the recommended method of running the Let's Encrypt
client beta releases on systems that don't have a packaged version. Debian
experimental, Arch linux and FreeBSD now have native packages, so on those
systems you can just install ``letsencrypt`` (and perhaps
``letsencrypt-apache``). If you'd like to run the latest copy from Git, or
run your own locally modified copy of the client, follow the instructions in
the :doc:`contributing`. Some `other methods of installation`_ are discussed
below.
Plugins
=======
=========== = = ===============================================================
Plugin A I Notes
=========== = = ===============================================================
apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on
Debian-based distributions with ``libaugeas0`` 1.0+.
standalone_ Y N Uses a "standalone" webserver to obtain a cert.
webroot_ Y N Obtains a cert using an already running webserver.
manual_ Y N Helps you obtain a cert by giving you instructions to perform
domain validation yourself.
nginx_ Y Y Very experimental and not included in letsencrypt-auto_.
=========== = = ===============================================================
The Let's Encrypt client supports a number of different "plugins" that can be
used to obtain and/or install certificates. Plugins that can obtain a cert
are called "authenticators" and can be used with the "certonly" command.
Plugins that can install a cert are called "installers". Plugins that do both
can be used with the "letsencrypt run" command, which is the default.
=========== ==== ==== ===============================================================
Plugin Auth Inst Notes
=========== ==== ==== ===============================================================
apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on
Debian-based distributions with ``libaugeas0`` 1.0+.
standalone_ Y N Uses a "standalone" webserver to obtain a cert.
webroot_ Y N Obtains a cert by writing to the webroot directory of an
already running webserver.
manual_ Y N Helps you obtain a cert by giving you instructions to perform
domain validation yourself.
nginx_ Y Y Very experimental and not included in letsencrypt-auto_.
=========== ==== ==== ===============================================================
Future plugins for IMAP servers, SMTP servers, IRC servers, etc, are likely to
be installers but not authenticators.
Apache
------
If you're running Apache 2.4 on a Debian-based OS with version 1.0+ of
the ``libaugeas0`` package available, you can use the Apache plugin.
This automates both obtaining and installing certs on an Apache
This automates both obtaining *and* installing certs on an Apache
webserver. To specify this plugin on the command line, simply include
``--apache``.
@@ -184,13 +120,22 @@ Webroot
If you're running a webserver that you don't want to stop to use
standalone, you can use the webroot plugin to obtain a cert by
including ``certonly`` and ``--webroot`` on the command line. In
addition, you'll need to specify ``--webroot-path`` with the root
addition, you'll need to specify ``--webroot-path`` or ``-w`` with the root
directory of the files served by your webserver. For example,
``--webroot-path /var/www/html`` or
``--webroot-path /usr/share/nginx/html`` are two common webroot paths.
If multiple domains are specified, they must all use the same path.
Additionally, your server must be configured to serve files from
hidden directories.
If you're getting a certificate for many domains at once, each domain will use
the most recent ``--webroot-path``. So for instance:
``letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/eg -d eg.is -d www.eg.is``
Would obtain a single certificate for all of those names, using the
``/var/www/example`` webroot directory for the first two, and
``/var/www/eg`` for the second two.
Note that to use the webroot plugin, your server must be configured to serve
files from hidden directories.
Manual
------
@@ -352,6 +297,107 @@ give us us as much information as possible:
- your operating system, including specific version
- specify which installation_ method you've chosen
Other methods of installation
=============================
Running with Docker
-------------------
Docker_ is an amazingly simple and quick way to obtain a
certificate. However, this mode of operation is unable to install
certificates or configure your webserver, because our installer
plugins cannot reach it from inside the Docker container.
You should definitely read the :ref:`where-certs` section, in order to
know how to manage the certs
manually. https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance
provides some information about recommended ciphersuites. If none of
these make much sense to you, you should definitely use the
letsencrypt-auto_ method, which enables you to use installer plugins
that cover both of those hard topics.
If you're still not convinced and have decided to use this method,
from the server that the domain you're requesting a cert for resolves
to, `install Docker`_, then issue the following command:
.. code-block:: shell
sudo docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \
-v "/etc/letsencrypt:/etc/letsencrypt" \
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
quay.io/letsencrypt/letsencrypt:latest auth
and follow the instructions (note that ``auth`` command is explicitly
used - no installer plugins involved). Your new cert will be available
in ``/etc/letsencrypt/live`` on the host.
.. _Docker: https://docker.com
.. _`install Docker`: https://docs.docker.com/userguide/
Operating System Packages
--------------------------
**FreeBSD**
* Port: ``cd /usr/ports/security/py-letsencrypt && make install clean``
* Package: ``pkg install py27-letsencrypt``
**Arch Linux**
.. code-block:: shell
sudo pacman -S letsencrypt letsencrypt-apache
**Debian Experimental**
If you run Debian unstable, you can install experimental letsencrypt packages.
Add the line ``deb http://ftp.us.debian.org/debian/ experimental main`` (or
the equivalent for your country) to ``/etc/apt/sources.list``, then run
.. code-block:: shell
sudo apt-get update
sudo apt-get -t experimental install letsencrypt python-letsencrypt-apache
If you don't want to use the Apache plugin, you can ommit the
``python-letsencrypt-apache`` package.
**Other Operating Systems**
OS packaging is an ongoing effort. If you'd like to package
Let's Encrypt client for your distribution of choice please have a
look at the :doc:`packaging`.
From source
-----------
Installation from source is only supported for developers and the
whole process is described in the :doc:`contributing`.
.. warning:: Please do **not** use ``python setup.py install`` or
``python pip install .``. Please do **not** attempt the
installation commands as superuser/root and/or without virtual
environment, e.g. ``sudo python setup.py install``, ``sudo pip
install``, ``sudo ./venv/bin/...``. These modes of operation might
corrupt your operating system and are **not supported** by the
Let's Encrypt team!
Comparison of different methods
-------------------------------
Unless you have a very specific requirements, we kindly ask you to use
the letsencrypt-auto_ method. It's the fastest, the most thoroughly
tested and the most reliable way of getting our software and the free
SSL certificates!
Beyond the methods discussed here, other methods may be possible, such as
installing Let's Encrypt directly with pip from PyPI or downloading a ZIP
archive from GitHub may be technically possible but are not presently
recommended or supported.
.. rubric:: Footnotes
-1
View File
@@ -8,7 +8,6 @@ email = foo@example.com
domains = example.com
text = True
agree-dev-preview = True
agree-tos = True
debug = True
# Unfortunately, it's not possible to specify "verbose" multiple times
@@ -6,7 +6,6 @@ import os
import re
import shutil
import socket
import subprocess
import time
import zope.interface
@@ -94,13 +93,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
help="Path to the Apache 'a2enmod' binary.")
add("dismod", default=constants.CLI_DEFAULTS["dismod"],
help="Path to the Apache 'a2enmod' binary.")
add("init-script", default=constants.CLI_DEFAULTS["init_script"],
help="Path to the Apache init script (used for server "
"reload).")
add("le-vhost-ext", default=constants.CLI_DEFAULTS["le_vhost_ext"],
help="SSL vhost configuration extension.")
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
help="Apache server root directory.")
le_util.add_deprecated_argument(add, "init-script", 1)
def __init__(self, *args, **kwargs):
"""Initialize an Apache Configurator.
@@ -139,8 +136,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
# Verify Apache is installed
for exe in (self.conf("ctl"), self.conf("enmod"),
self.conf("dismod"), self.conf("init-script")):
for exe in (self.conf("ctl"), self.conf("enmod"), self.conf("dismod")):
if not le_util.exe_exists(exe):
raise errors.NoInstallationError
@@ -243,13 +239,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if not vhost.enabled:
self.enable_site(vhost)
def choose_vhost(self, target_name):
def choose_vhost(self, target_name, temp=False):
"""Chooses a virtual host based on the given domain name.
If there is no clear virtual host to be selected, the user is prompted
with all available choices.
The returned vhost is guaranteed to have TLS enabled unless temp is
True. If temp is True, there is no such guarantee and the result is
not cached.
:param str target_name: domain name
:param bool temp: whether the vhost is only used temporarily
:returns: ssl vhost associated with name
:rtype: :class:`~letsencrypt_apache.obj.VirtualHost`
@@ -264,15 +265,17 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Try to find a reasonable vhost
vhost = self._find_best_vhost(target_name)
if vhost is not None:
if temp:
return vhost
if not vhost.ssl:
vhost = self.make_vhost_ssl(vhost)
self.assoc[target_name] = vhost
return vhost
return self._choose_vhost_from_list(target_name)
return self._choose_vhost_from_list(target_name, temp)
def _choose_vhost_from_list(self, target_name):
def _choose_vhost_from_list(self, target_name, temp=False):
# Select a vhost from a list
vhost = display_ops.select_vhost(target_name, self.vhosts)
if vhost is None:
@@ -281,7 +284,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"No vhost was selected. Please specify servernames "
"in the Apache config", target_name)
raise errors.PluginError("No vhost selected")
elif temp:
return vhost
elif not vhost.ssl:
addrs = self._get_proposed_addrs(vhost, "443")
# TODO: Conflicts is too conservative
@@ -1233,16 +1237,25 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
le_util.run_script([self.conf("enmod"), mod_name])
def restart(self):
"""Reloads apache server.
"""Runs a config test and reloads the Apache server.
.. todo:: This function will be converted to using reload
:raises .errors.MisconfigurationError: If unable to reload due
to a configuration problem, or if the reload subprocess
cannot be run.
:raises .errors.MisconfigurationError: If either the config test
or reload fails.
"""
return apache_reload(self.conf("init-script"))
self.config_test()
self._reload()
def _reload(self):
"""Reloads the Apache server.
:raises .errors.MisconfigurationError: If reload fails
"""
try:
le_util.run_script([self.conf("ctl"), "-k", "graceful"])
except errors.SubprocessError as err:
raise errors.MisconfigurationError(str(err))
def config_test(self): # pylint: disable=no-self-use
"""Check the configuration of Apache for errors.
@@ -1364,44 +1377,6 @@ def _get_mod_deps(mod_name):
return deps.get(mod_name, [])
def apache_reload(apache_init_script):
"""Reloads the Apache Server.
:param str apache_init_script: Path to the Apache init script.
.. todo:: Try to use reload instead. (This caused timing problems before)
.. todo:: On failure, this should be a recovery_routine call with another
reload. This will confuse and inhibit developers from testing code
though. This change should happen after
the ApacheConfigurator has been thoroughly tested. The function will
need to be moved into the class again. Perhaps
this version can live on... for testing purposes.
:raises .errors.MisconfigurationError: If unable to reload due to a
configuration problem, or if the reload subprocess cannot be run.
"""
try:
proc = subprocess.Popen([apache_init_script, "reload"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
except (OSError, ValueError):
logger.fatal(
"Unable to reload the Apache process with %s", apache_init_script)
raise errors.MisconfigurationError(
"Unable to reload Apache process with %s" % apache_init_script)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
# Enter recovery routine...
logger.error("Apache Reload Failed!\n%s\n%s", stdout, stderr)
raise errors.MisconfigurationError(
"Error while reloading Apache:\n%s\n%s" % (stdout, stderr))
def get_file_path(vhost_path):
"""Get file path from augeas_vhost_path.
@@ -7,7 +7,6 @@ CLI_DEFAULTS = dict(
ctl="apache2ctl",
enmod="a2enmod",
dismod="a2dismod",
init_script="/etc/init.d/apache2",
le_vhost_ext="-le-ssl.conf",
)
"""CLI defaults."""
@@ -139,6 +139,12 @@ class TwoVhost80Test(util.ApacheTest):
self.assertFalse(self.vh_truth[0].ssl)
self.assertTrue(chosen_vhost.ssl)
@mock.patch("letsencrypt_apache.display_ops.select_vhost")
def test_choose_vhost_select_vhost_with_temp(self, mock_select):
mock_select.return_value = self.vh_truth[0]
chosen_vhost = self.config.choose_vhost("none.com", temp=True)
self.assertEqual(self.vh_truth[0], chosen_vhost)
@mock.patch("letsencrypt_apache.display_ops.select_vhost")
def test_choose_vhost_select_vhost_conflicting_non_ssl(self, mock_select):
mock_select.return_value = self.vh_truth[3]
@@ -563,24 +569,13 @@ class TwoVhost80Test(util.ApacheTest):
mock_script.side_effect = errors.SubprocessError("Can't find program")
self.assertRaises(errors.PluginError, self.config.get_version)
@mock.patch("letsencrypt_apache.configurator.subprocess.Popen")
def test_restart(self, mock_popen):
"""These will be changed soon enough with reload."""
mock_popen().returncode = 0
mock_popen().communicate.return_value = ("", "")
@mock.patch("letsencrypt_apache.configurator.le_util.run_script")
def test_restart(self, _):
self.config.restart()
@mock.patch("letsencrypt_apache.configurator.subprocess.Popen")
def test_restart_bad_process(self, mock_popen):
mock_popen.side_effect = OSError
self.assertRaises(errors.MisconfigurationError, self.config.restart)
@mock.patch("letsencrypt_apache.configurator.subprocess.Popen")
def test_restart_failure(self, mock_popen):
mock_popen().communicate.return_value = ("", "")
mock_popen().returncode = 1
@mock.patch("letsencrypt_apache.configurator.le_util.run_script")
def test_restart_bad_process(self, mock_run_script):
mock_run_script.side_effect = [None, errors.SubprocessError]
self.assertRaises(errors.MisconfigurationError, self.config.restart)
@@ -75,11 +75,7 @@ def get_apache_configurator(
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
work_dir=work_dir)
with mock.patch("letsencrypt_apache.configurator."
"subprocess.Popen") as mock_popen:
# This indicates config_test passes
mock_popen().communicate.return_value = ("Fine output", "No problems")
mock_popen().returncode = 0
with mock.patch("letsencrypt_apache.configurator.le_util.run_script"):
with mock.patch("letsencrypt_apache.configurator.le_util."
"exe_exists") as mock_exe_exists:
mock_exe_exists.return_value = True
@@ -111,8 +111,7 @@ class ApacheTlsSni01(common.TLSSNI01):
def _get_addrs(self, achall):
"""Return the Apache addresses needed for TLS-SNI-01."""
vhost = self.configurator.choose_vhost(achall.domain)
vhost = self.configurator.choose_vhost(achall.domain, temp=True)
# TODO: Checkout _default_ rules.
addrs = set()
default_addr = obj.Addr(("*", str(
+1 -1
View File
@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.1.0.dev0'
version = '0.2.0.dev0'
install_requires = [
'acme=={0}'.format(version),
+3 -3
View File
@@ -104,7 +104,7 @@ DeterminePythonVersion() {
ExperimentalBootstrap "Python 2.6"
elif [ $PYVER -lt 26 ] ; then
echo "You have an ancient version of Python entombed in your operating system..."
echo "This isn't going to work."
echo "This isn't going to work; you'll need at least version 2.6."
exit 1
fi
}
@@ -147,9 +147,9 @@ then
elif uname | grep -iq FreeBSD ; then
ExperimentalBootstrap "FreeBSD" freebsd.sh "$SUDO"
elif uname | grep -iq Darwin ; then
ExperimentalBootstrap "Mac OS X" mac.sh
ExperimentalBootstrap "Mac OS X" mac.sh # homebrew doesn't normally run as root
elif grep -iq "Amazon Linux" /etc/issue ; then
ExperimentalBootstrap "Amazon Linux" _rpm_common.sh
ExperimentalBootstrap "Amazon Linux" _rpm_common.sh "$SUDO"
else
echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!"
echo
+1 -1
View File
@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.1.0.dev0'
version = '0.2.0.dev0'
install_requires = [
'letsencrypt=={0}'.format(version),
+1 -1
View File
@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.1.0.dev0'
version = '0.2.0.dev0'
install_requires = [
'acme=={0}'.format(version),
-5
View File
@@ -1,5 +0,0 @@
This is a PREVIEW RELEASE of a client application for the Let's Encrypt certificate authority and other services using the ACME protocol. The Let's Encrypt certificate authority is NOT YET ISSUING CERTIFICATES TO THE PUBLIC.
Until publicly-trusted certificates can be issued by Let's Encrypt, this software CANNOT OBTAIN A PUBLICLY-TRUSTED CERTIFICATE FOR YOUR WEB SERVER. You should only use this program if you are a developer interested in experimenting with the ACME protocol or in helping to improve this software. If you want to configure your web site with HTTPS in the meantime, please obtain a certificate from a different authority.
For updates on the status of Let's Encrypt, please visit the Let's Encrypt home page at https://letsencrypt.org/.
+1 -1
View File
@@ -1,4 +1,4 @@
"""Let's Encrypt client."""
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
__version__ = '0.1.0.dev0'
__version__ = '0.2.0.dev0'
+106 -48
View File
@@ -1,12 +1,14 @@
"""Let's Encrypt CLI."""
# TODO: Sanity check all input. Be sure to avoid shell code etc...
# pylint: disable=too-many-lines
# (TODO: split this file into main.py and cli.py)
import argparse
import atexit
import functools
import json
import logging
import logging.handlers
import os
import pkg_resources
import sys
import time
import traceback
@@ -59,6 +61,7 @@ the cert. Major SUBCOMMANDS are:
revoke Revoke a previously obtained certificate
rollback Rollback server configuration changes made during install
config_changes Show changes made to server config during installation
plugins Display information about installed plugins
"""
@@ -71,7 +74,7 @@ USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing c
%s
--webroot Place files in a server's webroot folder for authentication
OR use different servers to obtain (authenticate) the cert and then install it:
OR use different plugins to obtain (authenticate) the cert and then install it:
--authenticator standalone --installer apache
@@ -99,7 +102,7 @@ def usage_strings(plugins):
def _find_domains(args, installer):
if args.domains is None:
if not args.domains:
domains = display_ops.choose_names(installer)
else:
domains = args.domains
@@ -300,6 +303,14 @@ def _report_new_cert(cert_path, fullchain_path):
.format(and_chain, path, expiry))
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)
def _suggest_donate():
"Suggest a donation to support Let's Encrypt"
reporter_util = zope.component.getUtility(interfaces.IReporter)
msg = ("If like Let's Encrypt, please consider supporting our work by:\n\n"
"Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate\n"
"Donating to EFF: https://eff.org/donate-le\n\n")
reporter_util.add_message(msg, reporter_util.LOW_PRIORITY)
def _auth_from_domains(le_client, config, domains):
"""Authenticate and enroll certificate."""
@@ -382,7 +393,6 @@ def diagnose_configurator_problem(cfg_type, requested, plugins):
def choose_configurator_plugins(args, config, plugins, verb): # pylint: disable=too-many-branches
"""
Figure out which configurator we're going to use
:raises error.PluginSelectionError if there was a problem
"""
@@ -470,11 +480,13 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
else:
display_ops.success_renewal(domains)
_suggest_donate()
def obtain_cert(args, config, plugins):
"""Authenticate & obtain cert, but do not install it."""
if args.domains is not None and args.csr is not None:
if args.domains and args.csr is not None:
# TODO: --csr could have a priority, when --domains is
# supplied, check if CSR matches given domains?
return "--domains and --csr are mutually exclusive"
@@ -499,6 +511,8 @@ def obtain_cert(args, config, plugins):
domains = _find_domains(args, installer)
_auth_from_domains(le_client, config, domains)
_suggest_donate()
def install(args, config, plugins):
"""Install a previously obtained cert in a server."""
@@ -670,7 +684,6 @@ class HelpfulArgumentParser(object):
print usage
sys.exit(0)
self.visible_topics = self.determine_help_topics(self.help_arg)
#print self.visible_topics
self.groups = {} # elements are added by .add_group()
def parse_args(self):
@@ -683,31 +696,8 @@ class HelpfulArgumentParser(object):
parsed_args = self.parser.parse_args(self.args)
parsed_args.func = self.VERBS[self.verb]
parsed_args.domains = self._parse_domains(parsed_args.domains)
return parsed_args
def _parse_domains(self, domains):
"""Helper function for parse_args() that parses domains from a
(possibly) comma separated list and returns list of unique domains.
:param domains: List of domain flags
:type domains: `list` of `string`
:returns: List of unique domains
:rtype: `list` of `string`
"""
uniqd = None
if domains:
dlist = []
for domain in domains:
dlist.extend([d.strip() for d in domain.split(",")])
# Make sure we don't have duplicates
uniqd = [d for i, d in enumerate(dlist) if d not in dlist[:i]]
return uniqd
def determine_verb(self):
"""Determines the verb/subcommand provided by the user.
@@ -771,6 +761,20 @@ class HelpfulArgumentParser(object):
kwargs["help"] = argparse.SUPPRESS
self.parser.add_argument(*args, **kwargs)
def add_deprecated_argument(self, argument_name, num_args):
"""Adds a deprecated argument with the name argument_name.
Deprecated arguments are not shown in the help. If they are used
on the command line, a warning is shown stating that the
argument is deprecated and no other action is taken.
:param str argument_name: Name of deprecated argument.
:param int nargs: Number of arguments the option takes.
"""
le_util.add_deprecated_argument(
self.parser.add_argument, argument_name, num_args)
def add_group(self, topic, **kwargs):
"""
@@ -860,8 +864,8 @@ def prepare_and_parse_args(plugins, args):
# --domains is useful, because it can be stored in config
#for subparser in parser_run, parser_auth, parser_install:
# subparser.add_argument("domains", nargs="*", metavar="domain")
helpful.add(None, "-d", "--domains", dest="domains",
metavar="DOMAIN", action="append",
helpful.add(None, "-d", "--domains", "--domain", dest="domains",
metavar="DOMAIN", action=DomainFlagProcessor, default=[],
help="Domain names to apply. For multiple domains you can use "
"multiple -d flags or enter a comma separated list of domains "
"as a parameter.")
@@ -879,10 +883,7 @@ def prepare_and_parse_args(plugins, args):
helpful.add(
"automation", "--renew-by-default", action="store_true",
help="Select renewal by default when domains are a superset of a "
"a previously attained cert")
helpful.add(
"automation", "--agree-dev-preview", action="store_true",
help="Agree to the Let's Encrypt Developer Preview Disclaimer")
"previously attained cert")
helpful.add(
"automation", "--agree-tos", dest="tos", action="store_true",
help="Agree to the Let's Encrypt Subscriber Agreement")
@@ -947,6 +948,8 @@ def prepare_and_parse_args(plugins, args):
help="Require that all configuration files are owned by the current "
"user; only needed if your config is somewhere unsafe like /tmp/")
helpful.add_deprecated_argument("--agree-dev-preview", 0)
_create_subparsers(helpful)
_paths_parser(helpful)
# _plugins_parsing should be the last thing to act upon the main
@@ -967,8 +970,7 @@ def _create_subparsers(helpful):
help="Set a custom user agent string for the client. User agent strings allow "
"the CA to collect high level statistics about success rates by OS and "
"plugin. If you wish to hide your server OS version from the Let's "
'Encrypt server, set this to "".'
)
'Encrypt server, set this to "".')
helpful.add("certonly",
"--csr", type=read_file,
help="Path to a Certificate Signing Request (CSR) in DER"
@@ -1041,7 +1043,7 @@ def _plugins_parsing(helpful, plugins):
helpful.add_group(
"plugins", description="Let's Encrypt client supports an "
"extensible plugins architecture. See '%(prog)s plugins' for a "
"list of all available plugins and their names. You can force "
"list of all installed plugins and their names. You can force "
"a particular plugin by setting options provided below. Further "
"down this help message you will find plugin-specific options "
"(prefixed by --{plugin_name}).")
@@ -1070,6 +1072,61 @@ def _plugins_parsing(helpful, plugins):
helpful.add_plugin_args(plugins)
# These would normally be a flag within the webroot plugin, but because
# they are parsed in conjunction with --domains, they live here for
# legibiility. helpful.add_plugin_ags must be called first to add the
# "webroot" topic
helpful.add("webroot", "-w", "--webroot-path", action=WebrootPathProcessor,
help="public_html / webroot path. This can be specified multiple times to "
"handle different domains; each domain will have the webroot path that"
" preceded it. For instance: `-w /var/www/example -d example.com -d "
"www.example.com -w /var/www/thing -d thing.net -d m.thing.net`")
parse_dict = lambda s: dict(json.loads(s))
# --webroot-map still has some awkward properties, so it is undocumented
helpful.add("webroot", "--webroot-map", default={}, type=parse_dict,
help=argparse.SUPPRESS)
class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring
def __init__(self, *args, **kwargs):
self.domain_before_webroot = False
argparse.Action.__init__(self, *args, **kwargs)
def __call__(self, parser, config, webroot, option_string=None):
"""
Keep a record of --webroot-path / -w flags during processing, so that
we know which apply to which -d flags
"""
if config.webroot_path is None: # first -w flag encountered
config.webroot_path = []
# if any --domain flags preceded the first --webroot-path flag,
# apply that webroot path to those; subsequent entries in
# config.webroot_map are filled in by cli.DomainFlagProcessor
if config.domains:
self.domain_before_webroot = True
for d in config.domains:
config.webroot_map.setdefault(d, webroot)
elif self.domain_before_webroot:
# FIXME if you set domains in a config file, you should get a different error
# here, pointing you to --webroot-map
raise errors.Error("If you specify multiple webroot paths, one of "
"them must precede all domain flags")
config.webroot_path.append(webroot)
class DomainFlagProcessor(argparse.Action): # pylint: disable=missing-docstring
def __call__(self, parser, config, domain_arg, option_string=None):
"""
Process a new -d flag, helping the webroot plugin construct a map of
{domain : webrootpath} if -w / --webroot-path is in use
"""
for domain in (d.strip() for d in domain_arg.split(",")):
if domain not in config.domains:
config.domains.append(domain)
# Each domain has a webroot_path of the most recent -w flag
if config.webroot_path:
config.webroot_map[domain] = config.webroot_path[-1]
def setup_log_file_handler(args, logfile, fmt):
"""Setup file debug logging."""
@@ -1148,11 +1205,19 @@ def _handle_exception(exc_type, exc_value, trace, args):
if issubclass(exc_type, errors.Error):
sys.exit(exc_value)
else:
# Here we're passing a client or ACME error out to the client at the shell
# Tell the user a bit about what happened, without overwhelming
# them with a full traceback
msg = ("An unexpected error occurred.\n" +
traceback.format_exception_only(exc_type, exc_value)[0] +
"Please see the ")
err = traceback.format_exception_only(exc_type, exc_value)[0]
# Typical error from the ACME module:
# acme.messages.Error: urn:acme:error:malformed :: The request message was
# malformed :: Error creating new registration :: Validation of contact
# mailto:none@longrandomstring.biz failed: Server failure at resolver
if ("urn:acme" in err and ":: " in err
and args.verbose_count <= flag_default("verbose_count")):
# prune ACME error code, we have a human description
_code, _sep, err = err.partition(":: ")
msg = "An unexpected error occurred:\n" + err + "Please see the "
if args is None:
msg += "logfile '{0}' for more details.".format(logfile)
else:
@@ -1204,13 +1269,6 @@ def main(cli_args=sys.argv[1:]):
zope.component.provideUtility(report)
atexit.register(report.atexit_print_messages)
# TODO: remove developer preview prompt for the launch
if not config.agree_dev_preview:
disclaimer = pkg_resources.resource_string("letsencrypt", "DISCLAIMER")
if not zope.component.getUtility(interfaces.IDisplay).yesno(
disclaimer, "Agree", "Cancel"):
raise errors.Error("Must agree to TOS")
if not os.geteuid() == 0:
logger.warning(
"Root (sudo) is required to run most of letsencrypt functionality.")
+1 -1
View File
@@ -16,7 +16,7 @@ CLI_DEFAULTS = dict(
"letsencrypt", "cli.ini"),
],
verbose_count=-(logging.WARNING / 10),
server="https://acme-staging.api.letsencrypt.org/directory",
server="https://acme-v01.api.letsencrypt.org/directory",
rsa_key_size=2048,
rollback_checkpoints=1,
config_dir="/etc/letsencrypt",
+26 -1
View File
@@ -1,12 +1,14 @@
"""Utilities for all Let's Encrypt."""
import argparse
import collections
import errno
import logging
import os
import platform
import re
import subprocess
import stat
import subprocess
import sys
from letsencrypt import errors
@@ -255,3 +257,26 @@ def safe_email(email):
else:
logger.warn("Invalid email address: %s.", email)
return False
def add_deprecated_argument(add_argument, argument_name, nargs):
"""Adds a deprecated argument with the name argument_name.
Deprecated arguments are not shown in the help. If they are used on
the command line, a warning is shown stating that the argument is
deprecated and no other action is taken.
:param callable add_argument: Function that adds arguments to an
argument parser/group.
:param str argument_name: Name of deprecated argument.
:param nargs: Value for nargs when adding the argument to argparse.
"""
class ShowWarning(argparse.Action):
"""Action to log a warning when an argument is used."""
def __call__(self, unused1, unused2, unused3, option_string=None):
sys.stderr.write(
"Use of {0} is deprecated.\n".format(option_string))
add_argument(argument_name, action=ShowWarning,
help=argparse.SUPPRESS, nargs=nargs)
+51 -19
View File
@@ -2,6 +2,7 @@
import errno
import logging
import os
import stat
import zope.interface
@@ -33,7 +34,9 @@ to serve all files under specified web root ({0})."""
@classmethod
def add_parser_arguments(cls, add):
add("path", help="public_html / webroot path")
# --webroot-path and --webroot-map are added in cli.py because they
# are parsed in conjunction with --domains
pass
def get_chall_pref(self, domain): # pragma: no cover
# pylint: disable=missing-docstring,no-self-use,unused-argument
@@ -41,34 +44,54 @@ to serve all files under specified web root ({0})."""
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
self.full_root = None
self.full_roots = {}
def prepare(self): # pylint: disable=missing-docstring
path = self.conf("path")
if path is None:
path_map = self.conf("map")
if not path_map:
raise errors.PluginError("--{0} must be set".format(
self.option_name("path")))
if not os.path.isdir(path):
raise errors.PluginError(
path + " does not exist or is not a directory")
self.full_root = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH)
for name, path in path_map.items():
if not os.path.isdir(path):
raise errors.PluginError(path + " does not exist or is not a directory")
self.full_roots[name] = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH)
logger.debug("Creating root challenges validation dir at %s",
self.full_root)
try:
os.makedirs(self.full_root)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise errors.PluginError(
"Couldn't create root for http-01 "
"challenge responses: {0}", exception)
logger.debug("Creating root challenges validation dir at %s",
self.full_roots[name])
try:
os.makedirs(self.full_roots[name])
# Set permissions as parent directory (GH #1389)
# We don't use the parameters in makedirs because it
# may not always work
# https://stackoverflow.com/questions/5231901/permission-problems-when-creating-a-dir-with-os-makedirs-python
stat_path = os.stat(path)
filemode = stat.S_IMODE(stat_path.st_mode)
os.chmod(self.full_roots[name], filemode)
# Set owner and group, too
os.chown(self.full_roots[name], stat_path.st_uid,
stat_path.st_gid)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise errors.PluginError(
"Couldn't create root for {0} http-01 "
"challenge responses: {1}", name, exception)
def perform(self, achalls): # pylint: disable=missing-docstring
assert self.full_root is not None
assert self.full_roots, "Webroot plugin appears to be missing webroot map"
return [self._perform_single(achall) for achall in achalls]
def _path_for_achall(self, achall):
return os.path.join(self.full_root, achall.chall.encode("token"))
try:
path = self.full_roots[achall.domain]
except IndexError:
raise errors.PluginError("Missing --webroot-path for domain: {1}"
.format(achall.domain))
if not os.path.exists(path):
raise errors.PluginError("Mysteriously missing path {0} for domain: {1}"
.format(path, achall.domain))
return os.path.join(path, achall.chall.encode("token"))
def _perform_single(self, achall):
response, validation = achall.response_and_validation()
@@ -76,6 +99,15 @@ to serve all files under specified web root ({0})."""
logger.debug("Attempting to save validation to %s", path)
with open(path, "w") as validation_file:
validation_file.write(validation.encode())
# Set permissions as parent directory (GH #1389)
parent_path = self.full_roots[achall.domain]
stat_parent_path = os.stat(parent_path)
filemode = stat.S_IMODE(stat_parent_path.st_mode)
# Remove execution bit (not needed for this file)
os.chmod(path, filemode & ~stat.S_IEXEC)
os.chown(path, stat_parent_path.st_uid, stat_parent_path.st_gid)
return response
def cleanup(self, achalls): # pylint: disable=missing-docstring
+24 -3
View File
@@ -3,6 +3,7 @@ import os
import shutil
import tempfile
import unittest
import stat
import mock
@@ -23,7 +24,7 @@ class AuthenticatorTest(unittest.TestCase):
"""Tests for letsencrypt.plugins.webroot.Authenticator."""
achall = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.HTTP01_P, domain=None, account_key=KEY)
challb=acme_util.HTTP01_P, domain="thing.com", account_key=KEY)
def setUp(self):
from letsencrypt.plugins.webroot import Authenticator
@@ -31,7 +32,8 @@ class AuthenticatorTest(unittest.TestCase):
self.validation_path = os.path.join(
self.path, ".well-known", "acme-challenge",
"ZXZhR3hmQURzNnBTUmIyTEF2OUlaZjE3RHQzanV4R0orUEN0OTJ3citvQQ")
self.config = mock.MagicMock(webroot_path=self.path)
self.config = mock.MagicMock(webroot_path=self.path,
webroot_map={"thing.com": self.path})
self.auth = Authenticator(self.config, "webroot")
self.auth.prepare()
@@ -46,14 +48,16 @@ class AuthenticatorTest(unittest.TestCase):
def test_add_parser_arguments(self):
add = mock.MagicMock()
self.auth.add_parser_arguments(add)
self.assertEqual(1, add.call_count)
self.assertEqual(0, add.call_count) # became 0 when we moved the args to cli.py!
def test_prepare_bad_root(self):
self.config.webroot_path = os.path.join(self.path, "null")
self.config.webroot_map["thing.com"] = self.config.webroot_path
self.assertRaises(errors.PluginError, self.auth.prepare)
def test_prepare_missing_root(self):
self.config.webroot_path = None
self.config.webroot_map = {}
self.assertRaises(errors.PluginError, self.auth.prepare)
def test_prepare_full_root_exists(self):
@@ -66,6 +70,23 @@ class AuthenticatorTest(unittest.TestCase):
self.assertRaises(errors.PluginError, self.auth.prepare)
os.chmod(self.path, 0o700)
def test_prepare_permissions(self):
# Remove exec bit from permission check, so that it
# matches the file
self.auth.perform([self.achall])
parent_permissions = (stat.S_IMODE(os.stat(self.path).st_mode) &
~stat.S_IEXEC)
actual_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode)
self.assertEqual(parent_permissions, actual_permissions)
parent_gid = os.stat(self.path).st_gid
parent_uid = os.stat(self.path).st_uid
self.assertEqual(os.stat(self.validation_path).st_gid, parent_gid)
self.assertEqual(os.stat(self.validation_path).st_uid, parent_uid)
def test_perform_cleanup(self):
responses = self.auth.perform([self.achall])
self.assertEqual(1, len(responses))
+66 -23
View File
@@ -39,25 +39,27 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.config_dir = os.path.join(self.tmp_dir, 'config')
self.work_dir = os.path.join(self.tmp_dir, 'work')
self.logs_dir = os.path.join(self.tmp_dir, 'logs')
self.standard_args = ['--text', '--config-dir', self.config_dir,
'--work-dir', self.work_dir, '--logs-dir',
self.logs_dir, '--agree-dev-preview']
self.standard_args = ['--config-dir', self.config_dir,
'--work-dir', self.work_dir,
'--logs-dir', self.logs_dir, '--text']
def tearDown(self):
shutil.rmtree(self.tmp_dir)
def _call(self, args):
"Run the cli with output streams and actual client mocked out"
with mock.patch('letsencrypt.cli.client') as client:
ret, stdout, stderr = self._call_no_clientmock(args)
return ret, stdout, stderr, client
with mock.patch('letsencrypt.cli._suggest_donate'):
with mock.patch('letsencrypt.cli.client') as client:
ret, stdout, stderr = self._call_no_clientmock(args)
return ret, stdout, stderr, client
def _call_no_clientmock(self, args):
"Run the client with output streams mocked out"
args = self.standard_args + args
with mock.patch('letsencrypt.cli.sys.stdout') as stdout:
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
ret = cli.main(args[:]) # NOTE: parser can alter its args!
with mock.patch('letsencrypt.cli._suggest_donate'):
with mock.patch('letsencrypt.cli.sys.stdout') as stdout:
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
ret = cli.main(args[:]) # NOTE: parser can alter its args!
return ret, stdout, stderr
def _call_stdout(self, args):
@@ -66,9 +68,10 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
caller.
"""
args = self.standard_args + args
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
with mock.patch('letsencrypt.cli.client') as client:
ret = cli.main(args[:]) # NOTE: parser can alter its args!
with mock.patch('letsencrypt.cli._suggest_donate'):
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
with mock.patch('letsencrypt.cli.client') as client:
ret = cli.main(args[:]) # NOTE: parser can alter its args!
return ret, None, stderr, client
def test_no_flags(self):
@@ -180,8 +183,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
def test_configurator_selection(self, mock_exe_exists):
mock_exe_exists.return_value = True
real_plugins = disco.PluginsRegistry.find_all()
args = ['--agree-dev-preview', '--apache',
'--authenticator', 'standalone']
args = ['--apache', '--authenticator', 'standalone']
# This needed two calls to find_all(), which we're avoiding for now
# because of possible side effects:
@@ -236,7 +238,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self._call(['plugins'] + list(args))
@mock.patch('letsencrypt.cli.plugins_disco')
def test_plugins_no_args(self, mock_disco):
@mock.patch('letsencrypt.cli.HelpfulArgumentParser.determine_help_topics')
def test_plugins_no_args(self, _det, mock_disco):
ifaces = []
plugins = mock_disco.PluginsRegistry.find_all()
@@ -247,7 +250,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
stdout.write.called_once_with(str(filtered))
@mock.patch('letsencrypt.cli.plugins_disco')
def test_plugins_init(self, mock_disco):
@mock.patch('letsencrypt.cli.HelpfulArgumentParser.determine_help_topics')
def test_plugins_init(self, _det, mock_disco):
ifaces = []
plugins = mock_disco.PluginsRegistry.find_all()
@@ -261,10 +265,10 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
stdout.write.called_once_with(str(verified))
@mock.patch('letsencrypt.cli.plugins_disco')
def test_plugins_prepare(self, mock_disco):
@mock.patch('letsencrypt.cli.HelpfulArgumentParser.determine_help_topics')
def test_plugins_prepare(self, _det, mock_disco):
ifaces = []
plugins = mock_disco.PluginsRegistry.find_all()
_, stdout, _, _ = self._call(['plugins', '--init', '--prepare'])
plugins.visible.assert_called_once_with()
plugins.visible().ifaces.assert_called_once_with(ifaces)
@@ -339,9 +343,29 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
namespace = cli.prepare_and_parse_args(plugins, long_args)
self.assertEqual(namespace.domains, ['example.com', 'another.net'])
def test_parse_webroot(self):
plugins = disco.PluginsRegistry.find_all()
webroot_args = ['--webroot', '-w', '/var/www/example',
'-d', 'example.com,www.example.com', '-w', '/var/www/superfluous',
'-d', 'superfluo.us', '-d', 'www.superfluo.us']
namespace = cli.prepare_and_parse_args(plugins, webroot_args)
self.assertEqual(namespace.webroot_map, {
'example.com': '/var/www/example',
'www.example.com': '/var/www/example',
'www.superfluo.us': '/var/www/superfluous',
'superfluo.us': '/var/www/superfluous'})
webroot_args = ['-d', 'stray.example.com'] + webroot_args
self.assertRaises(errors.Error, cli.prepare_and_parse_args, plugins, webroot_args)
webroot_map_args = ['--webroot-map', '{"eg.com" : "/tmp"}']
namespace = cli.prepare_and_parse_args(plugins, webroot_map_args)
self.assertEqual(namespace.webroot_map, {u"eg.com": u"/tmp"})
@mock.patch('letsencrypt.cli._suggest_donate')
@mock.patch('letsencrypt.crypto_util.notAfter')
@mock.patch('letsencrypt.cli.zope.component.getUtility')
def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter):
def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter, _suggest):
cert_path = '/etc/letsencrypt/live/foo.bar'
date = '1970-01-01'
mock_notAfter().date.return_value = date
@@ -370,10 +394,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
mock_init.return_value = mock_client
self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
@mock.patch('letsencrypt.cli._suggest_donate')
@mock.patch('letsencrypt.cli.zope.component.getUtility')
@mock.patch('letsencrypt.cli._treat_as_renewal')
@mock.patch('letsencrypt.cli._init_le_client')
def test_certonly_renewal(self, mock_init, mock_renewal, mock_get_utility):
def test_certonly_renewal(self, mock_init, mock_renewal, mock_get_utility, _suggest):
cert_path = '/etc/letsencrypt/live/foo.bar/cert.pem'
chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
@@ -395,13 +420,14 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertTrue(
chain_path in mock_get_utility().add_message.call_args[0][0])
@mock.patch('letsencrypt.cli._suggest_donate')
@mock.patch('letsencrypt.crypto_util.notAfter')
@mock.patch('letsencrypt.cli.display_ops.pick_installer')
@mock.patch('letsencrypt.cli.zope.component.getUtility')
@mock.patch('letsencrypt.cli._init_le_client')
@mock.patch('letsencrypt.cli.record_chosen_plugins')
def test_certonly_csr(self, _rec, mock_init, mock_get_utility,
mock_pick_installer, mock_notAfter):
mock_pick_installer, mock_notAfter, _suggest):
cert_path = '/etc/letsencrypt/live/blahcert.pem'
date = '1970-01-01'
mock_notAfter().date.return_value = date
@@ -450,9 +476,14 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
@mock.patch('letsencrypt.cli.sys')
def test_handle_exception(self, mock_sys):
# pylint: disable=protected-access
from acme import messages
args = mock.MagicMock()
mock_open = mock.mock_open()
with mock.patch('letsencrypt.cli.open', mock_open, create=True):
exception = Exception('detail')
args.verbose_count = 1
cli._handle_exception(
Exception, exc_value=exception, trace=None, args=None)
mock_open().write.assert_called_once_with(''.join(
@@ -469,11 +500,23 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
mock_sys.exit.assert_any_call(''.join(
traceback.format_exception_only(errors.Error, error)))
args = mock.MagicMock(debug=False)
exception = messages.Error(detail='alpha', typ='urn:acme:error:triffid',
title='beta')
args = mock.MagicMock(debug=False, verbose_count=-3)
cli._handle_exception(
Exception, exc_value=Exception('detail'), trace=None, args=args)
messages.Error, exc_value=exception, trace=None, args=args)
error_msg = mock_sys.exit.call_args_list[-1][0][0]
self.assertTrue('unexpected error' in error_msg)
self.assertTrue('acme:error' not in error_msg)
self.assertTrue('alpha' in error_msg)
self.assertTrue('beta' in error_msg)
args = mock.MagicMock(debug=False, verbose_count=1)
cli._handle_exception(
messages.Error, exc_value=exception, trace=None, args=args)
error_msg = mock_sys.exit.call_args_list[-1][0][0]
self.assertTrue('unexpected error' in error_msg)
self.assertTrue('acme:error' in error_msg)
self.assertTrue('alpha' in error_msg)
interrupt = KeyboardInterrupt('detail')
cli._handle_exception(
+39
View File
@@ -1,8 +1,10 @@
"""Tests for letsencrypt.le_util."""
import argparse
import errno
import os
import shutil
import stat
import StringIO
import tempfile
import unittest
@@ -284,5 +286,42 @@ class SafeEmailTest(unittest.TestCase):
self.assertFalse(self._call(addr), "%s failed." % addr)
class AddDeprecatedArgumentTest(unittest.TestCase):
"""Test add_deprecated_argument."""
def setUp(self):
self.parser = argparse.ArgumentParser()
def _call(self, argument_name, nargs):
from letsencrypt.le_util import add_deprecated_argument
add_deprecated_argument(self.parser.add_argument, argument_name, nargs)
def test_warning_no_arg(self):
self._call("--old-option", 0)
stderr = self._get_argparse_warnings(["--old-option"])
self.assertTrue("--old-option is deprecated" in stderr)
def test_warning_with_arg(self):
self._call("--old-option", 1)
stderr = self._get_argparse_warnings(["--old-option", "42"])
self.assertTrue("--old-option is deprecated" in stderr)
def _get_argparse_warnings(self, args):
stderr = StringIO.StringIO()
with mock.patch("letsencrypt.le_util.sys.stderr", new=stderr):
self.parser.parse_args(args)
return stderr.getvalue()
def test_help(self):
self._call("--old-option", 2)
stdout = StringIO.StringIO()
with mock.patch("letsencrypt.le_util.sys.stdout", new=stdout):
try:
self.parser.parse_args(["-h"])
except SystemExit:
pass
self.assertTrue("--old-option" not in stdout.getvalue())
if __name__ == "__main__":
unittest.main() # pragma: no cover
+1 -1
View File
@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.1.0.dev0'
version = '0.2.0.dev0'
install_requires = [
'setuptools', # pkg_resources
-1
View File
@@ -21,7 +21,6 @@ letsencrypt_test () {
$store_flags \
--text \
--no-redirect \
--agree-dev-preview \
--agree-tos \
--register-unsafely-without-email \
--renew-by-default \
+4 -2
View File
@@ -1,10 +1,12 @@
#!/bin/sh -xe
# Release dev packages to PyPI
# Needed to fix problems with git signatures and pinentry
export GPG_TTY=$(tty)
version="0.0.0.dev$(date +%Y%m%d)"
DEV_RELEASE_BRANCH="dev-release"
# TODO: create a real release key instead of using Kuba's personal one
RELEASE_GPG_KEY="${RELEASE_GPG_KEY:-148C30F6F7E429337A72D992B00B9CC82D7ADF2C}"
RELEASE_GPG_KEY=A2CFB51FA275A7286234E7B24D17C995CD9775F2
# port for a local Python Package Index (used in testing)
PORT=${PORT:-1234}
+1 -1
View File
@@ -16,7 +16,7 @@ fi
cover () {
if [ "$1" = "letsencrypt" ]; then
min=98
min=97
elif [ "$1" = "acme" ]; then
min=100
elif [ "$1" = "letsencrypt_apache" ]; then