mirror of
https://github.com/certbot/certbot.git
synced 2026-08-04 12:21:51 +02:00
Merge remote-tracking branch 'upstream/master' into bugfix_redirect
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
letsencrypt/DISCLAIMER
|
|
||||||
+1
-1
@@ -33,7 +33,7 @@ RUN /opt/letsencrypt/src/ubuntu.sh && \
|
|||||||
# Dockerfile we make sure we cache as much as possible
|
# Dockerfile we make sure we cache as much as possible
|
||||||
|
|
||||||
# py26reqs.txt not installed!
|
# 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
|
# all above files are necessary for setup.py, however, package source
|
||||||
# code directory has to be copied separately to a subdirectory...
|
# code directory has to be copied separately to a subdirectory...
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ include CHANGES.rst
|
|||||||
include CONTRIBUTING.md
|
include CONTRIBUTING.md
|
||||||
include LICENSE.txt
|
include LICENSE.txt
|
||||||
include linter_plugin.py
|
include linter_plugin.py
|
||||||
include letsencrypt/DISCLAIMER
|
|
||||||
recursive-include docs *
|
recursive-include docs *
|
||||||
recursive-include examples *
|
recursive-include examples *
|
||||||
recursive-include letsencrypt/tests/testdata *
|
recursive-include letsencrypt/tests/testdata *
|
||||||
|
|||||||
+108
-66
@@ -3,12 +3,9 @@
|
|||||||
Disclaimer
|
Disclaimer
|
||||||
==========
|
==========
|
||||||
|
|
||||||
This is a **DEVELOPER PREVIEW** intended for developers and testers only.
|
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
|
||||||
**DO NOT RUN THIS CODE ON A PRODUCTION SERVER. IT WILL INSTALL CERTIFICATES
|
on production systems.
|
||||||
SIGNED BY A TEST CA, AND WILL CAUSE CERT WARNINGS FOR USERS.**
|
|
||||||
|
|
||||||
Browser-trusted certificates will be available in the coming months.
|
|
||||||
|
|
||||||
For more information regarding the status of the project, please see
|
For more information regarding the status of the project, please see
|
||||||
https://letsencrypt.org. Be sure to checkout the
|
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
|
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|
|
|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
|
.. |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
|
.. _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
|
Current Features
|
||||||
----------------
|
================
|
||||||
|
|
||||||
* Supports multiple web servers:
|
* Supports multiple web servers:
|
||||||
|
|
||||||
- apache/2.x (tested and working on Ubuntu Linux)
|
- apache/2.x (working on Debian 8+ and Ubuntu 12.04+)
|
||||||
- nginx/0.8.48+ (under development)
|
|
||||||
- standalone (runs its own simple webserver to prove you control a domain)
|
- 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.
|
* 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.
|
compliant services.
|
||||||
* Can get domain-validated (DV) certificates.
|
* Can get domain-validated (DV) certificates.
|
||||||
* Can revoke certificates.
|
* Can revoke certificates.
|
||||||
@@ -92,34 +158,10 @@ Current Features
|
|||||||
runs https only (Apache only)
|
runs https only (Apache only)
|
||||||
* Fully automated.
|
* Fully automated.
|
||||||
* Configuration changes are logged and can be reverted.
|
* 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.
|
* 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
|
.. _Freenode: https://freenode.net
|
||||||
.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev
|
.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ class JSONDeSerializable(object):
|
|||||||
:rtype: str
|
:rtype: str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.json_dumps(sort_keys=True, indent=4)
|
return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': '))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def json_dump_default(cls, python_object):
|
def json_dump_default(cls, python_object):
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"""Tests for acme.jose.interfaces."""
|
"""Tests for acme.jose.interfaces."""
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
class JSONDeSerializableTest(unittest.TestCase):
|
class JSONDeSerializableTest(unittest.TestCase):
|
||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
@@ -92,9 +90,8 @@ class JSONDeSerializableTest(unittest.TestCase):
|
|||||||
self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps())
|
self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps())
|
||||||
|
|
||||||
def test_json_dumps_pretty(self):
|
def test_json_dumps_pretty(self):
|
||||||
filler = ' ' if six.PY2 else ''
|
|
||||||
self.assertEqual(self.seq.json_dumps_pretty(),
|
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):
|
def test_json_dump_default(self):
|
||||||
from acme.jose.interfaces import JSONDeSerializable
|
from acme.jose.interfaces import JSONDeSerializable
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ from setuptools import setup
|
|||||||
from setuptools import find_packages
|
from setuptools import find_packages
|
||||||
|
|
||||||
|
|
||||||
version = '0.1.0.dev0'
|
version = '0.2.0.dev0'
|
||||||
|
|
||||||
install_requires = [
|
install_requires = [
|
||||||
# load_pem_private/public_key (>=0.6)
|
# load_pem_private/public_key (>=0.6)
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
# - Fedora 22, 23 (x64)
|
# - Fedora 22, 23 (x64)
|
||||||
# - Centos 7 (x64: onD igitalOcean droplet)
|
# - Centos 7 (x64: onD igitalOcean droplet)
|
||||||
|
|
||||||
if type yum 2>/dev/null
|
if type dnf 2>/dev/null
|
||||||
then
|
|
||||||
tool=yum
|
|
||||||
elif type dnf 2>/dev/null
|
|
||||||
then
|
then
|
||||||
tool=dnf
|
tool=dnf
|
||||||
|
elif type yum 2>/dev/null
|
||||||
|
then
|
||||||
|
tool=yum
|
||||||
|
|
||||||
else
|
else
|
||||||
echo "Neither yum nor dnf found. Aborting bootstrap!"
|
echo "Neither yum nor dnf found. Aborting bootstrap!"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -40,6 +41,7 @@ if ! $tool install -y \
|
|||||||
augeas-libs \
|
augeas-libs \
|
||||||
openssl-devel \
|
openssl-devel \
|
||||||
libffi-devel \
|
libffi-devel \
|
||||||
|
redhat-rpm-config \
|
||||||
ca-certificates
|
ca-certificates
|
||||||
then
|
then
|
||||||
echo "Could not install additional dependencies. Aborting bootstrap!"
|
echo "Could not install additional dependencies. Aborting bootstrap!"
|
||||||
|
|||||||
+138
-92
@@ -28,13 +28,8 @@ Firstly, please `install Git`_ and run the following commands:
|
|||||||
git clone https://github.com/letsencrypt/letsencrypt
|
git clone https://github.com/letsencrypt/letsencrypt
|
||||||
cd 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
|
.. _`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:
|
To install and run the client you just need to type:
|
||||||
|
|
||||||
@@ -61,107 +56,48 @@ or for full help, type:
|
|||||||
|
|
||||||
./letsencrypt-auto --help all
|
./letsencrypt-auto --help all
|
||||||
|
|
||||||
Running with Docker
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
Docker_ is an amazingly simple and quick way to obtain a
|
``letsencrypt-auto`` is the recommended method of running the Let's Encrypt
|
||||||
certificate. However, this mode of operation is unable to install
|
client beta releases on systems that don't have a packaged version. Debian
|
||||||
certificates or configure your webserver, because our installer
|
experimental, Arch linux and FreeBSD now have native packages, so on those
|
||||||
plugins cannot reach it from inside the Docker container.
|
systems you can just install ``letsencrypt`` (and perhaps
|
||||||
|
``letsencrypt-apache``). If you'd like to run the latest copy from Git, or
|
||||||
You should definitely read the :ref:`where-certs` section, in order to
|
run your own locally modified copy of the client, follow the instructions in
|
||||||
know how to manage the certs
|
the :doc:`contributing`. Some `other methods of installation`_ are discussed
|
||||||
manually. https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance
|
below.
|
||||||
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!
|
|
||||||
|
|
||||||
|
|
||||||
Plugins
|
Plugins
|
||||||
=======
|
=======
|
||||||
|
|
||||||
=========== = = ===============================================================
|
The Let's Encrypt client supports a number of different "plugins" that can be
|
||||||
Plugin A I Notes
|
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
|
apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on
|
||||||
Debian-based distributions with ``libaugeas0`` 1.0+.
|
Debian-based distributions with ``libaugeas0`` 1.0+.
|
||||||
standalone_ Y N Uses a "standalone" webserver to obtain a cert.
|
standalone_ Y N Uses a "standalone" webserver to obtain a cert.
|
||||||
webroot_ Y N Obtains a cert using an already running webserver.
|
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
|
manual_ Y N Helps you obtain a cert by giving you instructions to perform
|
||||||
domain validation yourself.
|
domain validation yourself.
|
||||||
nginx_ Y Y Very experimental and not included in letsencrypt-auto_.
|
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
|
Apache
|
||||||
------
|
------
|
||||||
|
|
||||||
If you're running Apache 2.4 on a Debian-based OS with version 1.0+ of
|
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.
|
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
|
webserver. To specify this plugin on the command line, simply include
|
||||||
``--apache``.
|
``--apache``.
|
||||||
|
|
||||||
@@ -184,13 +120,22 @@ Webroot
|
|||||||
If you're running a webserver that you don't want to stop to use
|
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
|
standalone, you can use the webroot plugin to obtain a cert by
|
||||||
including ``certonly`` and ``--webroot`` on the command line. In
|
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,
|
directory of the files served by your webserver. For example,
|
||||||
``--webroot-path /var/www/html`` or
|
``--webroot-path /var/www/html`` or
|
||||||
``--webroot-path /usr/share/nginx/html`` are two common webroot paths.
|
``--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
|
If you're getting a certificate for many domains at once, each domain will use
|
||||||
hidden directories.
|
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
|
Manual
|
||||||
------
|
------
|
||||||
@@ -352,6 +297,107 @@ give us us as much information as possible:
|
|||||||
- your operating system, including specific version
|
- your operating system, including specific version
|
||||||
- specify which installation_ method you've chosen
|
- 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
|
.. rubric:: Footnotes
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ email = foo@example.com
|
|||||||
domains = example.com
|
domains = example.com
|
||||||
|
|
||||||
text = True
|
text = True
|
||||||
agree-dev-preview = True
|
|
||||||
agree-tos = True
|
agree-tos = True
|
||||||
debug = True
|
debug = True
|
||||||
# Unfortunately, it's not possible to specify "verbose" multiple times
|
# Unfortunately, it's not possible to specify "verbose" multiple times
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import os
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import zope.interface
|
import zope.interface
|
||||||
@@ -94,13 +93,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
help="Path to the Apache 'a2enmod' binary.")
|
help="Path to the Apache 'a2enmod' binary.")
|
||||||
add("dismod", default=constants.CLI_DEFAULTS["dismod"],
|
add("dismod", default=constants.CLI_DEFAULTS["dismod"],
|
||||||
help="Path to the Apache 'a2enmod' binary.")
|
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"],
|
add("le-vhost-ext", default=constants.CLI_DEFAULTS["le_vhost_ext"],
|
||||||
help="SSL vhost configuration extension.")
|
help="SSL vhost configuration extension.")
|
||||||
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
|
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
|
||||||
help="Apache server root directory.")
|
help="Apache server root directory.")
|
||||||
|
le_util.add_deprecated_argument(add, "init-script", 1)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialize an Apache Configurator.
|
"""Initialize an Apache Configurator.
|
||||||
@@ -139,8 +136,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
# Verify Apache is installed
|
# Verify Apache is installed
|
||||||
for exe in (self.conf("ctl"), self.conf("enmod"),
|
for exe in (self.conf("ctl"), self.conf("enmod"), self.conf("dismod")):
|
||||||
self.conf("dismod"), self.conf("init-script")):
|
|
||||||
if not le_util.exe_exists(exe):
|
if not le_util.exe_exists(exe):
|
||||||
raise errors.NoInstallationError
|
raise errors.NoInstallationError
|
||||||
|
|
||||||
@@ -243,13 +239,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
if not vhost.enabled:
|
if not vhost.enabled:
|
||||||
self.enable_site(vhost)
|
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.
|
"""Chooses a virtual host based on the given domain name.
|
||||||
|
|
||||||
If there is no clear virtual host to be selected, the user is prompted
|
If there is no clear virtual host to be selected, the user is prompted
|
||||||
with all available choices.
|
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 str target_name: domain name
|
||||||
|
:param bool temp: whether the vhost is only used temporarily
|
||||||
|
|
||||||
:returns: ssl vhost associated with name
|
:returns: ssl vhost associated with name
|
||||||
:rtype: :class:`~letsencrypt_apache.obj.VirtualHost`
|
:rtype: :class:`~letsencrypt_apache.obj.VirtualHost`
|
||||||
@@ -264,15 +265,17 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
# Try to find a reasonable vhost
|
# Try to find a reasonable vhost
|
||||||
vhost = self._find_best_vhost(target_name)
|
vhost = self._find_best_vhost(target_name)
|
||||||
if vhost is not None:
|
if vhost is not None:
|
||||||
|
if temp:
|
||||||
|
return vhost
|
||||||
if not vhost.ssl:
|
if not vhost.ssl:
|
||||||
vhost = self.make_vhost_ssl(vhost)
|
vhost = self.make_vhost_ssl(vhost)
|
||||||
|
|
||||||
self.assoc[target_name] = vhost
|
self.assoc[target_name] = vhost
|
||||||
return 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
|
# Select a vhost from a list
|
||||||
vhost = display_ops.select_vhost(target_name, self.vhosts)
|
vhost = display_ops.select_vhost(target_name, self.vhosts)
|
||||||
if vhost is None:
|
if vhost is None:
|
||||||
@@ -281,7 +284,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
"No vhost was selected. Please specify servernames "
|
"No vhost was selected. Please specify servernames "
|
||||||
"in the Apache config", target_name)
|
"in the Apache config", target_name)
|
||||||
raise errors.PluginError("No vhost selected")
|
raise errors.PluginError("No vhost selected")
|
||||||
|
elif temp:
|
||||||
|
return vhost
|
||||||
elif not vhost.ssl:
|
elif not vhost.ssl:
|
||||||
addrs = self._get_proposed_addrs(vhost, "443")
|
addrs = self._get_proposed_addrs(vhost, "443")
|
||||||
# TODO: Conflicts is too conservative
|
# TODO: Conflicts is too conservative
|
||||||
@@ -1233,16 +1237,25 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
le_util.run_script([self.conf("enmod"), mod_name])
|
le_util.run_script([self.conf("enmod"), mod_name])
|
||||||
|
|
||||||
def restart(self):
|
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 either the config test
|
||||||
|
or reload fails.
|
||||||
:raises .errors.MisconfigurationError: If unable to reload due
|
|
||||||
to a configuration problem, or if the reload subprocess
|
|
||||||
cannot be run.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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
|
def config_test(self): # pylint: disable=no-self-use
|
||||||
"""Check the configuration of Apache for errors.
|
"""Check the configuration of Apache for errors.
|
||||||
@@ -1364,44 +1377,6 @@ def _get_mod_deps(mod_name):
|
|||||||
return deps.get(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):
|
def get_file_path(vhost_path):
|
||||||
"""Get file path from augeas_vhost_path.
|
"""Get file path from augeas_vhost_path.
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ CLI_DEFAULTS = dict(
|
|||||||
ctl="apache2ctl",
|
ctl="apache2ctl",
|
||||||
enmod="a2enmod",
|
enmod="a2enmod",
|
||||||
dismod="a2dismod",
|
dismod="a2dismod",
|
||||||
init_script="/etc/init.d/apache2",
|
|
||||||
le_vhost_ext="-le-ssl.conf",
|
le_vhost_ext="-le-ssl.conf",
|
||||||
)
|
)
|
||||||
"""CLI defaults."""
|
"""CLI defaults."""
|
||||||
|
|||||||
@@ -139,6 +139,12 @@ class TwoVhost80Test(util.ApacheTest):
|
|||||||
self.assertFalse(self.vh_truth[0].ssl)
|
self.assertFalse(self.vh_truth[0].ssl)
|
||||||
self.assertTrue(chosen_vhost.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")
|
@mock.patch("letsencrypt_apache.display_ops.select_vhost")
|
||||||
def test_choose_vhost_select_vhost_conflicting_non_ssl(self, mock_select):
|
def test_choose_vhost_select_vhost_conflicting_non_ssl(self, mock_select):
|
||||||
mock_select.return_value = self.vh_truth[3]
|
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")
|
mock_script.side_effect = errors.SubprocessError("Can't find program")
|
||||||
self.assertRaises(errors.PluginError, self.config.get_version)
|
self.assertRaises(errors.PluginError, self.config.get_version)
|
||||||
|
|
||||||
@mock.patch("letsencrypt_apache.configurator.subprocess.Popen")
|
@mock.patch("letsencrypt_apache.configurator.le_util.run_script")
|
||||||
def test_restart(self, mock_popen):
|
def test_restart(self, _):
|
||||||
"""These will be changed soon enough with reload."""
|
|
||||||
mock_popen().returncode = 0
|
|
||||||
mock_popen().communicate.return_value = ("", "")
|
|
||||||
|
|
||||||
self.config.restart()
|
self.config.restart()
|
||||||
|
|
||||||
@mock.patch("letsencrypt_apache.configurator.subprocess.Popen")
|
@mock.patch("letsencrypt_apache.configurator.le_util.run_script")
|
||||||
def test_restart_bad_process(self, mock_popen):
|
def test_restart_bad_process(self, mock_run_script):
|
||||||
mock_popen.side_effect = OSError
|
mock_run_script.side_effect = [None, errors.SubprocessError]
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
self.assertRaises(errors.MisconfigurationError, self.config.restart)
|
self.assertRaises(errors.MisconfigurationError, self.config.restart)
|
||||||
|
|
||||||
|
|||||||
@@ -75,11 +75,7 @@ def get_apache_configurator(
|
|||||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
||||||
work_dir=work_dir)
|
work_dir=work_dir)
|
||||||
|
|
||||||
with mock.patch("letsencrypt_apache.configurator."
|
with mock.patch("letsencrypt_apache.configurator.le_util.run_script"):
|
||||||
"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."
|
with mock.patch("letsencrypt_apache.configurator.le_util."
|
||||||
"exe_exists") as mock_exe_exists:
|
"exe_exists") as mock_exe_exists:
|
||||||
mock_exe_exists.return_value = True
|
mock_exe_exists.return_value = True
|
||||||
|
|||||||
@@ -111,8 +111,7 @@ class ApacheTlsSni01(common.TLSSNI01):
|
|||||||
|
|
||||||
def _get_addrs(self, achall):
|
def _get_addrs(self, achall):
|
||||||
"""Return the Apache addresses needed for TLS-SNI-01."""
|
"""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.
|
# TODO: Checkout _default_ rules.
|
||||||
addrs = set()
|
addrs = set()
|
||||||
default_addr = obj.Addr(("*", str(
|
default_addr = obj.Addr(("*", str(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
|||||||
from setuptools import find_packages
|
from setuptools import find_packages
|
||||||
|
|
||||||
|
|
||||||
version = '0.1.0.dev0'
|
version = '0.2.0.dev0'
|
||||||
|
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme=={0}'.format(version),
|
'acme=={0}'.format(version),
|
||||||
|
|||||||
+3
-3
@@ -104,7 +104,7 @@ DeterminePythonVersion() {
|
|||||||
ExperimentalBootstrap "Python 2.6"
|
ExperimentalBootstrap "Python 2.6"
|
||||||
elif [ $PYVER -lt 26 ] ; then
|
elif [ $PYVER -lt 26 ] ; then
|
||||||
echo "You have an ancient version of Python entombed in your operating system..."
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -147,9 +147,9 @@ then
|
|||||||
elif uname | grep -iq FreeBSD ; then
|
elif uname | grep -iq FreeBSD ; then
|
||||||
ExperimentalBootstrap "FreeBSD" freebsd.sh "$SUDO"
|
ExperimentalBootstrap "FreeBSD" freebsd.sh "$SUDO"
|
||||||
elif uname | grep -iq Darwin ; then
|
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
|
elif grep -iq "Amazon Linux" /etc/issue ; then
|
||||||
ExperimentalBootstrap "Amazon Linux" _rpm_common.sh
|
ExperimentalBootstrap "Amazon Linux" _rpm_common.sh "$SUDO"
|
||||||
else
|
else
|
||||||
echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!"
|
echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!"
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
|||||||
from setuptools import find_packages
|
from setuptools import find_packages
|
||||||
|
|
||||||
|
|
||||||
version = '0.1.0.dev0'
|
version = '0.2.0.dev0'
|
||||||
|
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'letsencrypt=={0}'.format(version),
|
'letsencrypt=={0}'.format(version),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
|||||||
from setuptools import find_packages
|
from setuptools import find_packages
|
||||||
|
|
||||||
|
|
||||||
version = '0.1.0.dev0'
|
version = '0.2.0.dev0'
|
||||||
|
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'acme=={0}'.format(version),
|
'acme=={0}'.format(version),
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
"""Let's Encrypt client."""
|
"""Let's Encrypt client."""
|
||||||
|
|
||||||
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
|
# 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
@@ -1,12 +1,14 @@
|
|||||||
"""Let's Encrypt CLI."""
|
"""Let's Encrypt CLI."""
|
||||||
# TODO: Sanity check all input. Be sure to avoid shell code etc...
|
# 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 argparse
|
||||||
import atexit
|
import atexit
|
||||||
import functools
|
import functools
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
import pkg_resources
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -59,6 +61,7 @@ the cert. Major SUBCOMMANDS are:
|
|||||||
revoke Revoke a previously obtained certificate
|
revoke Revoke a previously obtained certificate
|
||||||
rollback Rollback server configuration changes made during install
|
rollback Rollback server configuration changes made during install
|
||||||
config_changes Show changes made to server config during installation
|
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
|
%s
|
||||||
--webroot Place files in a server's webroot folder for authentication
|
--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
|
--authenticator standalone --installer apache
|
||||||
|
|
||||||
@@ -99,7 +102,7 @@ def usage_strings(plugins):
|
|||||||
|
|
||||||
|
|
||||||
def _find_domains(args, installer):
|
def _find_domains(args, installer):
|
||||||
if args.domains is None:
|
if not args.domains:
|
||||||
domains = display_ops.choose_names(installer)
|
domains = display_ops.choose_names(installer)
|
||||||
else:
|
else:
|
||||||
domains = args.domains
|
domains = args.domains
|
||||||
@@ -300,6 +303,14 @@ def _report_new_cert(cert_path, fullchain_path):
|
|||||||
.format(and_chain, path, expiry))
|
.format(and_chain, path, expiry))
|
||||||
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)
|
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):
|
def _auth_from_domains(le_client, config, domains):
|
||||||
"""Authenticate and enroll certificate."""
|
"""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
|
def choose_configurator_plugins(args, config, plugins, verb): # pylint: disable=too-many-branches
|
||||||
"""
|
"""
|
||||||
Figure out which configurator we're going to use
|
Figure out which configurator we're going to use
|
||||||
|
|
||||||
:raises error.PluginSelectionError if there was a problem
|
: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:
|
else:
|
||||||
display_ops.success_renewal(domains)
|
display_ops.success_renewal(domains)
|
||||||
|
|
||||||
|
_suggest_donate()
|
||||||
|
|
||||||
|
|
||||||
def obtain_cert(args, config, plugins):
|
def obtain_cert(args, config, plugins):
|
||||||
"""Authenticate & obtain cert, but do not install it."""
|
"""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
|
# TODO: --csr could have a priority, when --domains is
|
||||||
# supplied, check if CSR matches given domains?
|
# supplied, check if CSR matches given domains?
|
||||||
return "--domains and --csr are mutually exclusive"
|
return "--domains and --csr are mutually exclusive"
|
||||||
@@ -499,6 +511,8 @@ def obtain_cert(args, config, plugins):
|
|||||||
domains = _find_domains(args, installer)
|
domains = _find_domains(args, installer)
|
||||||
_auth_from_domains(le_client, config, domains)
|
_auth_from_domains(le_client, config, domains)
|
||||||
|
|
||||||
|
_suggest_donate()
|
||||||
|
|
||||||
|
|
||||||
def install(args, config, plugins):
|
def install(args, config, plugins):
|
||||||
"""Install a previously obtained cert in a server."""
|
"""Install a previously obtained cert in a server."""
|
||||||
@@ -670,7 +684,6 @@ class HelpfulArgumentParser(object):
|
|||||||
print usage
|
print usage
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
self.visible_topics = self.determine_help_topics(self.help_arg)
|
self.visible_topics = self.determine_help_topics(self.help_arg)
|
||||||
#print self.visible_topics
|
|
||||||
self.groups = {} # elements are added by .add_group()
|
self.groups = {} # elements are added by .add_group()
|
||||||
|
|
||||||
def parse_args(self):
|
def parse_args(self):
|
||||||
@@ -683,31 +696,8 @@ class HelpfulArgumentParser(object):
|
|||||||
parsed_args = self.parser.parse_args(self.args)
|
parsed_args = self.parser.parse_args(self.args)
|
||||||
parsed_args.func = self.VERBS[self.verb]
|
parsed_args.func = self.VERBS[self.verb]
|
||||||
|
|
||||||
parsed_args.domains = self._parse_domains(parsed_args.domains)
|
|
||||||
return parsed_args
|
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):
|
def determine_verb(self):
|
||||||
"""Determines the verb/subcommand provided by the user.
|
"""Determines the verb/subcommand provided by the user.
|
||||||
@@ -771,6 +761,20 @@ class HelpfulArgumentParser(object):
|
|||||||
kwargs["help"] = argparse.SUPPRESS
|
kwargs["help"] = argparse.SUPPRESS
|
||||||
self.parser.add_argument(*args, **kwargs)
|
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):
|
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
|
# --domains is useful, because it can be stored in config
|
||||||
#for subparser in parser_run, parser_auth, parser_install:
|
#for subparser in parser_run, parser_auth, parser_install:
|
||||||
# subparser.add_argument("domains", nargs="*", metavar="domain")
|
# subparser.add_argument("domains", nargs="*", metavar="domain")
|
||||||
helpful.add(None, "-d", "--domains", dest="domains",
|
helpful.add(None, "-d", "--domains", "--domain", dest="domains",
|
||||||
metavar="DOMAIN", action="append",
|
metavar="DOMAIN", action=DomainFlagProcessor, default=[],
|
||||||
help="Domain names to apply. For multiple domains you can use "
|
help="Domain names to apply. For multiple domains you can use "
|
||||||
"multiple -d flags or enter a comma separated list of domains "
|
"multiple -d flags or enter a comma separated list of domains "
|
||||||
"as a parameter.")
|
"as a parameter.")
|
||||||
@@ -879,10 +883,7 @@ def prepare_and_parse_args(plugins, args):
|
|||||||
helpful.add(
|
helpful.add(
|
||||||
"automation", "--renew-by-default", action="store_true",
|
"automation", "--renew-by-default", action="store_true",
|
||||||
help="Select renewal by default when domains are a superset of a "
|
help="Select renewal by default when domains are a superset of a "
|
||||||
"a previously attained cert")
|
"previously attained cert")
|
||||||
helpful.add(
|
|
||||||
"automation", "--agree-dev-preview", action="store_true",
|
|
||||||
help="Agree to the Let's Encrypt Developer Preview Disclaimer")
|
|
||||||
helpful.add(
|
helpful.add(
|
||||||
"automation", "--agree-tos", dest="tos", action="store_true",
|
"automation", "--agree-tos", dest="tos", action="store_true",
|
||||||
help="Agree to the Let's Encrypt Subscriber Agreement")
|
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 "
|
help="Require that all configuration files are owned by the current "
|
||||||
"user; only needed if your config is somewhere unsafe like /tmp/")
|
"user; only needed if your config is somewhere unsafe like /tmp/")
|
||||||
|
|
||||||
|
helpful.add_deprecated_argument("--agree-dev-preview", 0)
|
||||||
|
|
||||||
_create_subparsers(helpful)
|
_create_subparsers(helpful)
|
||||||
_paths_parser(helpful)
|
_paths_parser(helpful)
|
||||||
# _plugins_parsing should be the last thing to act upon the main
|
# _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 "
|
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 "
|
"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 "
|
"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",
|
helpful.add("certonly",
|
||||||
"--csr", type=read_file,
|
"--csr", type=read_file,
|
||||||
help="Path to a Certificate Signing Request (CSR) in DER"
|
help="Path to a Certificate Signing Request (CSR) in DER"
|
||||||
@@ -1041,7 +1043,7 @@ def _plugins_parsing(helpful, plugins):
|
|||||||
helpful.add_group(
|
helpful.add_group(
|
||||||
"plugins", description="Let's Encrypt client supports an "
|
"plugins", description="Let's Encrypt client supports an "
|
||||||
"extensible plugins architecture. See '%(prog)s plugins' for a "
|
"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 "
|
"a particular plugin by setting options provided below. Further "
|
||||||
"down this help message you will find plugin-specific options "
|
"down this help message you will find plugin-specific options "
|
||||||
"(prefixed by --{plugin_name}).")
|
"(prefixed by --{plugin_name}).")
|
||||||
@@ -1070,6 +1072,61 @@ def _plugins_parsing(helpful, plugins):
|
|||||||
|
|
||||||
helpful.add_plugin_args(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):
|
def setup_log_file_handler(args, logfile, fmt):
|
||||||
"""Setup file debug logging."""
|
"""Setup file debug logging."""
|
||||||
@@ -1148,11 +1205,19 @@ def _handle_exception(exc_type, exc_value, trace, args):
|
|||||||
if issubclass(exc_type, errors.Error):
|
if issubclass(exc_type, errors.Error):
|
||||||
sys.exit(exc_value)
|
sys.exit(exc_value)
|
||||||
else:
|
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
|
# Tell the user a bit about what happened, without overwhelming
|
||||||
# them with a full traceback
|
# them with a full traceback
|
||||||
msg = ("An unexpected error occurred.\n" +
|
err = traceback.format_exception_only(exc_type, exc_value)[0]
|
||||||
traceback.format_exception_only(exc_type, exc_value)[0] +
|
# Typical error from the ACME module:
|
||||||
"Please see the ")
|
# 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:
|
if args is None:
|
||||||
msg += "logfile '{0}' for more details.".format(logfile)
|
msg += "logfile '{0}' for more details.".format(logfile)
|
||||||
else:
|
else:
|
||||||
@@ -1204,13 +1269,6 @@ def main(cli_args=sys.argv[1:]):
|
|||||||
zope.component.provideUtility(report)
|
zope.component.provideUtility(report)
|
||||||
atexit.register(report.atexit_print_messages)
|
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:
|
if not os.geteuid() == 0:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Root (sudo) is required to run most of letsencrypt functionality.")
|
"Root (sudo) is required to run most of letsencrypt functionality.")
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ CLI_DEFAULTS = dict(
|
|||||||
"letsencrypt", "cli.ini"),
|
"letsencrypt", "cli.ini"),
|
||||||
],
|
],
|
||||||
verbose_count=-(logging.WARNING / 10),
|
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,
|
rsa_key_size=2048,
|
||||||
rollback_checkpoints=1,
|
rollback_checkpoints=1,
|
||||||
config_dir="/etc/letsencrypt",
|
config_dir="/etc/letsencrypt",
|
||||||
|
|||||||
+26
-1
@@ -1,12 +1,14 @@
|
|||||||
"""Utilities for all Let's Encrypt."""
|
"""Utilities for all Let's Encrypt."""
|
||||||
|
import argparse
|
||||||
import collections
|
import collections
|
||||||
import errno
|
import errno
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
import stat
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
from letsencrypt import errors
|
from letsencrypt import errors
|
||||||
|
|
||||||
@@ -255,3 +257,26 @@ def safe_email(email):
|
|||||||
else:
|
else:
|
||||||
logger.warn("Invalid email address: %s.", email)
|
logger.warn("Invalid email address: %s.", email)
|
||||||
return False
|
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)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import errno
|
import errno
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
import zope.interface
|
import zope.interface
|
||||||
|
|
||||||
@@ -33,7 +34,9 @@ to serve all files under specified web root ({0})."""
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_parser_arguments(cls, add):
|
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
|
def get_chall_pref(self, domain): # pragma: no cover
|
||||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
# 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):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Authenticator, self).__init__(*args, **kwargs)
|
super(Authenticator, self).__init__(*args, **kwargs)
|
||||||
self.full_root = None
|
self.full_roots = {}
|
||||||
|
|
||||||
def prepare(self): # pylint: disable=missing-docstring
|
def prepare(self): # pylint: disable=missing-docstring
|
||||||
path = self.conf("path")
|
path_map = self.conf("map")
|
||||||
if path is None:
|
|
||||||
|
if not path_map:
|
||||||
raise errors.PluginError("--{0} must be set".format(
|
raise errors.PluginError("--{0} must be set".format(
|
||||||
self.option_name("path")))
|
self.option_name("path")))
|
||||||
|
for name, path in path_map.items():
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
raise errors.PluginError(
|
raise errors.PluginError(path + " does not exist or is not a directory")
|
||||||
path + " does not exist or is not a directory")
|
self.full_roots[name] = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH)
|
||||||
self.full_root = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH)
|
|
||||||
|
|
||||||
logger.debug("Creating root challenges validation dir at %s",
|
logger.debug("Creating root challenges validation dir at %s",
|
||||||
self.full_root)
|
self.full_roots[name])
|
||||||
try:
|
try:
|
||||||
os.makedirs(self.full_root)
|
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:
|
except OSError as exception:
|
||||||
if exception.errno != errno.EEXIST:
|
if exception.errno != errno.EEXIST:
|
||||||
raise errors.PluginError(
|
raise errors.PluginError(
|
||||||
"Couldn't create root for http-01 "
|
"Couldn't create root for {0} http-01 "
|
||||||
"challenge responses: {0}", exception)
|
"challenge responses: {1}", name, exception)
|
||||||
|
|
||||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
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]
|
return [self._perform_single(achall) for achall in achalls]
|
||||||
|
|
||||||
def _path_for_achall(self, achall):
|
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):
|
def _perform_single(self, achall):
|
||||||
response, validation = achall.response_and_validation()
|
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)
|
logger.debug("Attempting to save validation to %s", path)
|
||||||
with open(path, "w") as validation_file:
|
with open(path, "w") as validation_file:
|
||||||
validation_file.write(validation.encode())
|
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
|
return response
|
||||||
|
|
||||||
def cleanup(self, achalls): # pylint: disable=missing-docstring
|
def cleanup(self, achalls): # pylint: disable=missing-docstring
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
import stat
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
"""Tests for letsencrypt.plugins.webroot.Authenticator."""
|
"""Tests for letsencrypt.plugins.webroot.Authenticator."""
|
||||||
|
|
||||||
achall = achallenges.KeyAuthorizationAnnotatedChallenge(
|
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):
|
def setUp(self):
|
||||||
from letsencrypt.plugins.webroot import Authenticator
|
from letsencrypt.plugins.webroot import Authenticator
|
||||||
@@ -31,7 +32,8 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
self.validation_path = os.path.join(
|
self.validation_path = os.path.join(
|
||||||
self.path, ".well-known", "acme-challenge",
|
self.path, ".well-known", "acme-challenge",
|
||||||
"ZXZhR3hmQURzNnBTUmIyTEF2OUlaZjE3RHQzanV4R0orUEN0OTJ3citvQQ")
|
"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 = Authenticator(self.config, "webroot")
|
||||||
self.auth.prepare()
|
self.auth.prepare()
|
||||||
|
|
||||||
@@ -46,14 +48,16 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
def test_add_parser_arguments(self):
|
def test_add_parser_arguments(self):
|
||||||
add = mock.MagicMock()
|
add = mock.MagicMock()
|
||||||
self.auth.add_parser_arguments(add)
|
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):
|
def test_prepare_bad_root(self):
|
||||||
self.config.webroot_path = os.path.join(self.path, "null")
|
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)
|
self.assertRaises(errors.PluginError, self.auth.prepare)
|
||||||
|
|
||||||
def test_prepare_missing_root(self):
|
def test_prepare_missing_root(self):
|
||||||
self.config.webroot_path = None
|
self.config.webroot_path = None
|
||||||
|
self.config.webroot_map = {}
|
||||||
self.assertRaises(errors.PluginError, self.auth.prepare)
|
self.assertRaises(errors.PluginError, self.auth.prepare)
|
||||||
|
|
||||||
def test_prepare_full_root_exists(self):
|
def test_prepare_full_root_exists(self):
|
||||||
@@ -66,6 +70,23 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
self.assertRaises(errors.PluginError, self.auth.prepare)
|
self.assertRaises(errors.PluginError, self.auth.prepare)
|
||||||
os.chmod(self.path, 0o700)
|
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):
|
def test_perform_cleanup(self):
|
||||||
responses = self.auth.perform([self.achall])
|
responses = self.auth.perform([self.achall])
|
||||||
self.assertEqual(1, len(responses))
|
self.assertEqual(1, len(responses))
|
||||||
|
|||||||
@@ -39,15 +39,16 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
self.config_dir = os.path.join(self.tmp_dir, 'config')
|
self.config_dir = os.path.join(self.tmp_dir, 'config')
|
||||||
self.work_dir = os.path.join(self.tmp_dir, 'work')
|
self.work_dir = os.path.join(self.tmp_dir, 'work')
|
||||||
self.logs_dir = os.path.join(self.tmp_dir, 'logs')
|
self.logs_dir = os.path.join(self.tmp_dir, 'logs')
|
||||||
self.standard_args = ['--text', '--config-dir', self.config_dir,
|
self.standard_args = ['--config-dir', self.config_dir,
|
||||||
'--work-dir', self.work_dir, '--logs-dir',
|
'--work-dir', self.work_dir,
|
||||||
self.logs_dir, '--agree-dev-preview']
|
'--logs-dir', self.logs_dir, '--text']
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
shutil.rmtree(self.tmp_dir)
|
shutil.rmtree(self.tmp_dir)
|
||||||
|
|
||||||
def _call(self, args):
|
def _call(self, args):
|
||||||
"Run the cli with output streams and actual client mocked out"
|
"Run the cli with output streams and actual client mocked out"
|
||||||
|
with mock.patch('letsencrypt.cli._suggest_donate'):
|
||||||
with mock.patch('letsencrypt.cli.client') as client:
|
with mock.patch('letsencrypt.cli.client') as client:
|
||||||
ret, stdout, stderr = self._call_no_clientmock(args)
|
ret, stdout, stderr = self._call_no_clientmock(args)
|
||||||
return ret, stdout, stderr, client
|
return ret, stdout, stderr, client
|
||||||
@@ -55,6 +56,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
def _call_no_clientmock(self, args):
|
def _call_no_clientmock(self, args):
|
||||||
"Run the client with output streams mocked out"
|
"Run the client with output streams mocked out"
|
||||||
args = self.standard_args + args
|
args = self.standard_args + args
|
||||||
|
with mock.patch('letsencrypt.cli._suggest_donate'):
|
||||||
with mock.patch('letsencrypt.cli.sys.stdout') as stdout:
|
with mock.patch('letsencrypt.cli.sys.stdout') as stdout:
|
||||||
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
|
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
|
||||||
ret = cli.main(args[:]) # NOTE: parser can alter its args!
|
ret = cli.main(args[:]) # NOTE: parser can alter its args!
|
||||||
@@ -66,6 +68,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
caller.
|
caller.
|
||||||
"""
|
"""
|
||||||
args = self.standard_args + args
|
args = self.standard_args + args
|
||||||
|
with mock.patch('letsencrypt.cli._suggest_donate'):
|
||||||
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
|
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
|
||||||
with mock.patch('letsencrypt.cli.client') as client:
|
with mock.patch('letsencrypt.cli.client') as client:
|
||||||
ret = cli.main(args[:]) # NOTE: parser can alter its args!
|
ret = cli.main(args[:]) # NOTE: parser can alter its args!
|
||||||
@@ -180,8 +183,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
def test_configurator_selection(self, mock_exe_exists):
|
def test_configurator_selection(self, mock_exe_exists):
|
||||||
mock_exe_exists.return_value = True
|
mock_exe_exists.return_value = True
|
||||||
real_plugins = disco.PluginsRegistry.find_all()
|
real_plugins = disco.PluginsRegistry.find_all()
|
||||||
args = ['--agree-dev-preview', '--apache',
|
args = ['--apache', '--authenticator', 'standalone']
|
||||||
'--authenticator', 'standalone']
|
|
||||||
|
|
||||||
# This needed two calls to find_all(), which we're avoiding for now
|
# This needed two calls to find_all(), which we're avoiding for now
|
||||||
# because of possible side effects:
|
# because of possible side effects:
|
||||||
@@ -236,7 +238,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
self._call(['plugins'] + list(args))
|
self._call(['plugins'] + list(args))
|
||||||
|
|
||||||
@mock.patch('letsencrypt.cli.plugins_disco')
|
@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 = []
|
ifaces = []
|
||||||
plugins = mock_disco.PluginsRegistry.find_all()
|
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))
|
stdout.write.called_once_with(str(filtered))
|
||||||
|
|
||||||
@mock.patch('letsencrypt.cli.plugins_disco')
|
@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 = []
|
ifaces = []
|
||||||
plugins = mock_disco.PluginsRegistry.find_all()
|
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))
|
stdout.write.called_once_with(str(verified))
|
||||||
|
|
||||||
@mock.patch('letsencrypt.cli.plugins_disco')
|
@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 = []
|
ifaces = []
|
||||||
plugins = mock_disco.PluginsRegistry.find_all()
|
plugins = mock_disco.PluginsRegistry.find_all()
|
||||||
|
|
||||||
_, stdout, _, _ = self._call(['plugins', '--init', '--prepare'])
|
_, stdout, _, _ = self._call(['plugins', '--init', '--prepare'])
|
||||||
plugins.visible.assert_called_once_with()
|
plugins.visible.assert_called_once_with()
|
||||||
plugins.visible().ifaces.assert_called_once_with(ifaces)
|
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)
|
namespace = cli.prepare_and_parse_args(plugins, long_args)
|
||||||
self.assertEqual(namespace.domains, ['example.com', 'another.net'])
|
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.crypto_util.notAfter')
|
||||||
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
@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'
|
cert_path = '/etc/letsencrypt/live/foo.bar'
|
||||||
date = '1970-01-01'
|
date = '1970-01-01'
|
||||||
mock_notAfter().date.return_value = date
|
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
|
mock_init.return_value = mock_client
|
||||||
self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
|
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.zope.component.getUtility')
|
||||||
@mock.patch('letsencrypt.cli._treat_as_renewal')
|
@mock.patch('letsencrypt.cli._treat_as_renewal')
|
||||||
@mock.patch('letsencrypt.cli._init_le_client')
|
@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'
|
cert_path = '/etc/letsencrypt/live/foo.bar/cert.pem'
|
||||||
chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.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(
|
self.assertTrue(
|
||||||
chain_path in mock_get_utility().add_message.call_args[0][0])
|
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.crypto_util.notAfter')
|
||||||
@mock.patch('letsencrypt.cli.display_ops.pick_installer')
|
@mock.patch('letsencrypt.cli.display_ops.pick_installer')
|
||||||
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
||||||
@mock.patch('letsencrypt.cli._init_le_client')
|
@mock.patch('letsencrypt.cli._init_le_client')
|
||||||
@mock.patch('letsencrypt.cli.record_chosen_plugins')
|
@mock.patch('letsencrypt.cli.record_chosen_plugins')
|
||||||
def test_certonly_csr(self, _rec, mock_init, mock_get_utility,
|
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'
|
cert_path = '/etc/letsencrypt/live/blahcert.pem'
|
||||||
date = '1970-01-01'
|
date = '1970-01-01'
|
||||||
mock_notAfter().date.return_value = date
|
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')
|
@mock.patch('letsencrypt.cli.sys')
|
||||||
def test_handle_exception(self, mock_sys):
|
def test_handle_exception(self, mock_sys):
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
from acme import messages
|
||||||
|
|
||||||
|
args = mock.MagicMock()
|
||||||
mock_open = mock.mock_open()
|
mock_open = mock.mock_open()
|
||||||
|
|
||||||
with mock.patch('letsencrypt.cli.open', mock_open, create=True):
|
with mock.patch('letsencrypt.cli.open', mock_open, create=True):
|
||||||
exception = Exception('detail')
|
exception = Exception('detail')
|
||||||
|
args.verbose_count = 1
|
||||||
cli._handle_exception(
|
cli._handle_exception(
|
||||||
Exception, exc_value=exception, trace=None, args=None)
|
Exception, exc_value=exception, trace=None, args=None)
|
||||||
mock_open().write.assert_called_once_with(''.join(
|
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(
|
mock_sys.exit.assert_any_call(''.join(
|
||||||
traceback.format_exception_only(errors.Error, error)))
|
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(
|
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]
|
error_msg = mock_sys.exit.call_args_list[-1][0][0]
|
||||||
self.assertTrue('unexpected error' in error_msg)
|
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')
|
interrupt = KeyboardInterrupt('detail')
|
||||||
cli._handle_exception(
|
cli._handle_exception(
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"""Tests for letsencrypt.le_util."""
|
"""Tests for letsencrypt.le_util."""
|
||||||
|
import argparse
|
||||||
import errno
|
import errno
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
|
import StringIO
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@@ -284,5 +286,42 @@ class SafeEmailTest(unittest.TestCase):
|
|||||||
self.assertFalse(self._call(addr), "%s failed." % addr)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main() # pragma: no cover
|
unittest.main() # pragma: no cover
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
|||||||
from setuptools import find_packages
|
from setuptools import find_packages
|
||||||
|
|
||||||
|
|
||||||
version = '0.1.0.dev0'
|
version = '0.2.0.dev0'
|
||||||
|
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'setuptools', # pkg_resources
|
'setuptools', # pkg_resources
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ letsencrypt_test () {
|
|||||||
$store_flags \
|
$store_flags \
|
||||||
--text \
|
--text \
|
||||||
--no-redirect \
|
--no-redirect \
|
||||||
--agree-dev-preview \
|
|
||||||
--agree-tos \
|
--agree-tos \
|
||||||
--register-unsafely-without-email \
|
--register-unsafely-without-email \
|
||||||
--renew-by-default \
|
--renew-by-default \
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
#!/bin/sh -xe
|
#!/bin/sh -xe
|
||||||
# Release dev packages to PyPI
|
# 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)"
|
version="0.0.0.dev$(date +%Y%m%d)"
|
||||||
DEV_RELEASE_BRANCH="dev-release"
|
DEV_RELEASE_BRANCH="dev-release"
|
||||||
# TODO: create a real release key instead of using Kuba's personal one
|
RELEASE_GPG_KEY=A2CFB51FA275A7286234E7B24D17C995CD9775F2
|
||||||
RELEASE_GPG_KEY="${RELEASE_GPG_KEY:-148C30F6F7E429337A72D992B00B9CC82D7ADF2C}"
|
|
||||||
|
|
||||||
# port for a local Python Package Index (used in testing)
|
# port for a local Python Package Index (used in testing)
|
||||||
PORT=${PORT:-1234}
|
PORT=${PORT:-1234}
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ fi
|
|||||||
|
|
||||||
cover () {
|
cover () {
|
||||||
if [ "$1" = "letsencrypt" ]; then
|
if [ "$1" = "letsencrypt" ]; then
|
||||||
min=98
|
min=97
|
||||||
elif [ "$1" = "acme" ]; then
|
elif [ "$1" = "acme" ]; then
|
||||||
min=100
|
min=100
|
||||||
elif [ "$1" = "letsencrypt_apache" ]; then
|
elif [ "$1" = "letsencrypt_apache" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user