Compare commits

..

27 Commits

Author SHA1 Message Date
Brad Warren
8842c1e7eb quiet 2018-07-05 08:25:40 -07:00
Brad Warren
142efc9458 Use Python 3.7 for Python 3 macOS tests. 2018-07-02 16:25:20 -07:00
Brad Warren
0cee44f590 Merge pull request #6039 from certbot/handle-merge-conflicts
Pull .travis.yml changes into test-everything
2018-05-29 13:49:00 -07:00
Brad Warren
93d85920b9 Merge branch 'master' into handle-merge-conflicts 2018-05-25 08:15:23 -07:00
ohemorange
681d36d1d6 Merge pull request #5926 from certbot/update-test-everything
Add 2nd mypy test to test-everything
2018-05-04 14:22:59 -07:00
Brad Warren
71c7f09327 Merge branch 'master' into update-test-everything 2018-05-04 12:42:26 -07:00
ohemorange
6d82061b42 Merge pull request #5880 from certbot/mypy-everything
Merge master into test-everything
2018-04-16 23:51:40 -07:00
Brad Warren
a4a8b6e994 Merge branch 'master' into mypy-everything 2018-04-16 08:44:40 -07:00
ohemorange
9defbc3aed Add before_install to all tests running on test-everything (#5806)
* add before_install to all tests running on test-everything

* actually always run before_install

* also don't override addons

* only install libaugeas0 for docker_dev
2018-03-29 17:18:15 -07:00
Brad Warren
f1461946b8 Merge pull request #5688 from certbot/update-test-everything
Update the test-everything branch
2018-03-11 15:45:14 -07:00
Brad Warren
b7472fb337 Merge branch 'master' into update-test-everything 2018-03-08 11:30:09 -08:00
Brad Warren
937e27db28 Merge branch 'master' into update-test-everything 2018-03-07 15:57:07 -08:00
Brad Warren
e64137fb53 Merge pull request #5668 from certbot/update-test-everything
Fix merge conflicts and add more ACMEv2 integration tests
2018-03-06 09:19:25 -08:00
Brad Warren
a0dc7dafda add python3 ACMEv2 integration tests 2018-03-05 12:02:05 -08:00
Brad Warren
02975c84d9 set up nginx-oldest integration tests 2018-03-05 12:01:26 -08:00
Brad Warren
5307600a03 use v1 for clarity 2018-03-05 12:00:44 -08:00
Brad Warren
b0ce7529aa Merge branch 'master' into update-test-everything 2018-03-05 12:00:07 -08:00
Joona Hoikkala
5abec9fa3c Python3 now installed as default let's upgrade instead of trying to install (#5665) 2018-03-05 10:01:46 -08:00
Brad Warren
8753959bb3 Merge pull request #5645 from certbot/update-test-everything
Update the test-everything branch
2018-03-02 11:17:01 -08:00
Brad Warren
184b384b58 Merge branch 'master' into update-test-everything 2018-03-01 10:21:55 -08:00
Brad Warren
13b6b3f1b3 Merge pull request #5401 from certbot/fix-everything
Fix test-everything
2018-01-09 18:33:13 -08:00
Brad Warren
f4a1547481 Merge branch 'master' into fix-everything 2018-01-09 17:33:17 -08:00
Brad Warren
164121fc15 Revert "Update test-everything (#5397)"
This reverts commit 349643c9b8.
2018-01-09 17:32:07 -08:00
Brad Warren
349643c9b8 Update test-everything (#5397)
* Use josepy instead of acme.jose. (#5203)

* Parse variables without whitespace separator correctly in CentOS family of distributions (#5318)

* Pin josepy in letsencrypt-auto (#5321)

* pin josepy in le-auto

* Put pinned versions in sorted order

* Pin dependencies in oldest tests (#5316)

* Add tools/merge_requirements.py

* Revert "Fix oldest tests by pinning Google DNS deps (#5000)"

This reverts commit f68fba2be2.

* Add tools/oldest_constraints.txt

* Remove oldest constraints from tox.ini

* Rename dev constraints file

* Update tools/pip_install.sh

* Update install_and_test.sh

* Fix pip_install.sh

* Don't cat when you can cp

* Add ng-httpsclient to dev constraints for oldest tests

* Bump tested setuptools version

* Update dev_constraints comment

* Better document oldest dependencies

* test against oldest versions we say we require

* Update dev constraints

* Properly handle empty lines

* Update constraints gen in pip_install

* Remove duplicated zope.component

* Reduce pyasn1-modules dependency

* Remove blank line

* pin back google-api-python-client

* pin back uritemplate

* pin josepy for oldest tests

* Undo changes to install_and_test.sh

* Update install_and_test.sh description

* use split instead of partition

* More pip dependency resolution workarounds (#5339)

* remove pyopenssl and six deps

* remove outdated tox.ini dep requirement

* Fix auto_tests on systems with new bootstrappers (#5348)

* Fix pytest on macOS in Travis (#5360)

* Add tools/pytest.sh

* pass TRAVIS through in tox.ini

* Use tools/pytest.sh to run pytest

* Add quiet to pytest.ini

* ignore pytest cache

* print as a string (#5359)

* Use apache2ctl modules for Gentoo systems. (#5349)

* Do not call Apache binary for module reset in cleanup()

* Use apache2ctl modules for Gentoo

* Broader git ignore for pytest cache files (#5361)

Make gitignore take pytest cache directories in to account, even if
they reside in subdirectories.

If pytest is run for a certain module, ie. `pytest certbot-apache` the
cache directory is created under `certbot-apache` directory.

* Fix letsencrypt-auto name and long forms of -n (#5375)

* Deprecate Python2.6 by using Python3 on CentOS/RHEL 6 (#5329)

* If there's no python or there's only python2.6 on red hat systems, install python3

* Always check for python2.6

* address style, documentation, nits

* factor out all initialization code

* fix up python version return value when no python installed

* add no python error and exit

* document DeterminePythonVersion parameters

* build letsencrypt-auto

* close brace

* build leauto

* fix syntax errors

* set USE_PYTHON_3 for all cases

* rip out NOCRASH

* replace NOCRASH, update LE_PYTHON set logic

* use built-in venv for py3

* switch to LE_PYTHON not affecting bootstrap selection and not overwriting LE_PYTHON

* python3ify fetch.py

* get fetch.py working with python2 and 3

* don't verify server certificates in fetch.py HttpsGetter

* Use SSLContext and an environment variable so that our tests continue to never verify server certificates.

* typo

* build

* remove commented out code

* address review comments

* add documentation for YES_FLAG and QUIET_FLAG

* Add tests to centos6 Dockerfile to make sure we install python3 if and only if appropriate to do so.

* Allow non-interactive revocation without deleting certificates (#5386)

* Add --delete-after-revoke flags

* Use delete_after_revoke value

* Add delete_after_revoke unit tests

* Add integration tests for delete-after-revoke.

* Have letsencrypt-auto do a real upgrade in leauto-upgrades option 2 (#5390)

* Make leauto_upgrades do a real upgrade

* Cleanup vars and output

* Sleep until the server is ready

* add simple_http_server.py

* Use a randomly assigned port

* s/realpath/readlink

* wait for server before getting port

* s/localhost/all interfaces

* update Apache ciphersuites (#5383)

* Fix macOS builds for Python2.7 in Travis (#5378)

* Add OSX Python2 tests

* Make sure python2 is originating from homebrew on macOS

* Upgrade the already installed python2 instead of trying to reinstall
2018-01-09 17:24:14 -08:00
Brad Warren
899b56514a Merge pull request #5315 from certbot/update-test-everything
Update the test everything branch
2017-12-08 17:48:21 -08:00
Brad Warren
fa3bc6d774 Merge branch 'master' into test-everything 2017-12-08 16:37:28 -08:00
Jacob Hoffman-Andrews
76f5f590ea Add back macOS and Boulder for test-everything.
This commit should not be merged to master (if that happens, just revert it).

In https://github.com/certbot/certbot/pull/5270, we removed the macOS and
most Boulder integration tests from .travis.yml because they made builds very
slow. We still want to run those tests nightly, though. The test-everything
branch will have the tests that are too expensive to run for every PR.
2017-12-01 16:12:44 -08:00
112 changed files with 529 additions and 4003 deletions

View File

@@ -41,7 +41,7 @@ load-plugins=linter_plugin
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=fixme,locally-disabled,locally-enabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import,duplicate-code
disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import,duplicate-code
# abstract-class-not-used cannot be disabled locally (at least in
# pylint 1.4.1), same for abstract-class-little-used

View File

@@ -5,7 +5,14 @@ cache:
- $HOME/.cache/pip
before_install:
- '([ $TRAVIS_OS_NAME == linux ] && dpkg -s libaugeas0) || (brew update && brew install augeas python3 && brew upgrade python && brew link python)'
# In the test-everything branch, we merge master before running tests.
# This is because we want test-everything to test the code in master nightly
# in a Travis cron, but with a different set of tests than master has
# in .travis.yml.
- cp .travis.yml /tmp/travis.yml
- git pull origin master --strategy=recursive --strategy-option=theirs --no-edit
- if ! git diff .travis.yml /tmp/travis.yml ; then echo "Please merge master into test-everything"; exit 1; fi
- '([ $TRAVIS_OS_NAME == linux ] && dpkg -s libaugeas0) || (brew update && brew install augeas && brew upgrade python python3 && brew link python)'
before_script:
- 'if [ $TRAVIS_OS_NAME = osx ] ; then ulimit -n 1024 ; fi'
@@ -13,7 +20,15 @@ before_script:
matrix:
include:
- python: "2.7"
env: TOXENV=py27_install BOULDER_INTEGRATION=v1
env: TOXENV=cover FYI="this also tests py27"
- python: "2.7"
env: TOXENV=lint
- python: "2.7"
env: TOXENV=py27-nginx-oldest BOULDER_INTEGRATION=v1
sudo: required
services: docker
- python: "2.7"
env: TOXENV=py27-nginx-oldest BOULDER_INTEGRATION=v2
sudo: required
services: docker
- python: "2.7"
@@ -21,46 +36,75 @@ matrix:
sudo: required
services: docker
- python: "2.7"
env: TOXENV=cover FYI="this also tests py27"
env: TOXENV=py27_install BOULDER_INTEGRATION=v1
sudo: required
services: docker
- python: "2.7"
env: TOXENV='py27-{acme,apache,certbot,dns}-oldest'
- sudo: required
env: TOXENV=apache_compat
services: docker
- sudo: required
env: TOXENV=nginx_compat
services: docker
before_install:
- sudo: required
env: TOXENV=le_auto_precise
services: docker
- sudo: required
env: TOXENV=le_auto_trusty
services: docker
- sudo: required
env: TOXENV=le_auto_wheezy
services: docker
- sudo: required
env: TOXENV=le_auto_centos6
services: docker
- sudo: required
env: TOXENV=docker_dev
services: docker
addons:
apt:
packages: # don't install nginx and apache
- libaugeas0
- python: "2.7"
env: TOXENV=lint
env: TOXENV=apacheconftest
sudo: required
- python: "3.4"
env: TOXENV=py34 BOULDER_INTEGRATION=v1
sudo: required
services: docker
- python: "3.4"
env: TOXENV=mypy
- python: "3.5"
env: TOXENV=mypy
- python: "2.7"
env: TOXENV='py27-{acme,apache,certbot,dns,nginx}-oldest'
sudo: required
services: docker
- python: "3.4"
env: TOXENV=py34
env: TOXENV=py34 BOULDER_INTEGRATION=v2
sudo: required
services: docker
- python: "3.7"
dist: xenial
env: TOXENV=py37
- python: "3.5"
env: TOXENV=py35 BOULDER_INTEGRATION=v1
sudo: required
services: docker
- sudo: required
env: TOXENV=apache_compat
services: docker
before_install:
addons:
- sudo: required
env: TOXENV=le_auto_trusty
services: docker
before_install:
addons:
- python: "2.7"
env: TOXENV=apacheconftest
- python: "3.5"
env: TOXENV=py35 BOULDER_INTEGRATION=v2
sudo: required
services: docker
- python: "3.6"
env: TOXENV=py36 BOULDER_INTEGRATION=v1
sudo: required
services: docker
- python: "3.6"
env: TOXENV=py36 BOULDER_INTEGRATION=v2
sudo: required
services: docker
- python: "2.7"
env: TOXENV=nginxroundtrip
- language: generic
env: TOXENV=py27
os: osx
- language: generic
env: TOXENV=py37
os: osx
# Only build pushes to the master branch, PRs, and branches beginning with
@@ -78,6 +122,8 @@ sudo: false
addons:
apt:
sources:
- augeas
packages: # Keep in sync with letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh and Boulder.
- python-dev
- python-virtualenv
@@ -89,6 +135,10 @@ addons:
# For certbot-nginx integration testing
- nginx-light
- openssl
# for apacheconftest
- apache2
- libapache2-mod-wsgi
- libapache2-mod-macro
install: "travis_retry $(command -v pip || command -v pip3) install tox coveralls"
script:

View File

@@ -2,92 +2,6 @@
Certbot adheres to [Semantic Versioning](http://semver.org/).
## 0.25.1 - 2018-06-13
### Fixed
* TLS-ALPN-01 support has been removed from our acme library. Using our current
dependencies, we are unable to provide a correct implementation of this
challenge so we decided to remove it from the library until we can provide
proper support.
* Issues causing test failures when running the tests in the acme package with
pytest<3.0 has been resolved.
* certbot-nginx now correctly depends on acme>=0.25.0.
Despite us having broken lockstep, we are continuing to release new versions of
all Certbot components during releases for the time being, however, the only
packages with changes other than their version number were:
* acme
* certbot-nginx
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/56?closed=1
## 0.25.0 - 2018-06-06
### Added
* Support for the ready status type was added to acme. Without this change,
Certbot and acme users will begin encountering errors when using Let's
Encrypt's ACMEv2 API starting on June 19th for the staging environment and
July 5th for production. See
https://community.letsencrypt.org/t/acmev2-order-ready-status/62866 for more
information.
* Certbot now accepts the flag --reuse-key which will cause the same key to be
used in the certificate when the lineage is renewed rather than generating a
new key.
* You can now add multiple email addresses to your ACME account with Certbot by
providing a comma separated list of emails to the --email flag.
* Support for Let's Encrypt's upcoming TLS-ALPN-01 challenge was added to acme.
For more information, see
https://community.letsencrypt.org/t/tls-alpn-validation-method/63814/1.
* acme now supports specifying the source address to bind to when sending
outgoing connections. You still cannot specify this address using Certbot.
* If you run Certbot against Let's Encrypt's ACMEv2 staging server but don't
already have an account registered at that server URL, Certbot will
automatically reuse your staging account from Let's Encrypt's ACMEv1 endpoint
if it exists.
* Interfaces were added to Certbot allowing plugins to be called at additional
points. The `GenericUpdater` interface allows plugins to perform actions
every time `certbot renew` is run, regardless of whether any certificates are
due for renewal, and the `RenewDeployer` interface allows plugins to perform
actions when a certificate is renewed. See `certbot.interfaces` for more
information.
### Changed
* When running Certbot with --dry-run and you don't already have a staging
account, the created account does not contain an email address even if one
was provided to avoid expiration emails from Let's Encrypt's staging server.
* certbot-nginx does a better job of automatically detecting the location of
Nginx's configuration files when run on BSD based systems.
* acme now requires and uses pytest when running tests with setuptools with
`python setup.py test`.
* `certbot config_changes` no longer waits for user input before exiting.
### Fixed
* Misleading log output that caused users to think that Certbot's standalone
plugin failed to bind to a port when performing a challenge has been
corrected.
* An issue where certbot-nginx would fail to enable HSTS if the server block
already had an `add_header` directive has been resolved.
* certbot-nginx now does a better job detecting the server block to base the
configuration for TLS-SNI challenges on.
Despite us having broken lockstep, we are continuing to release new versions of
all Certbot components during releases for the time being, however, the only
packages with functional changes were:
* acme
* certbot
* certbot-apache
* certbot-nginx
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/54?closed=1
## 0.24.0 - 2018-05-02
### Added

View File

@@ -1,6 +1,5 @@
include LICENSE.txt
include README.rst
include pytest.ini
recursive-include docs *
recursive-include examples *
recursive-include acme/testdata *

View File

@@ -507,21 +507,6 @@ class TLSSNI01(KeyAuthorizationChallenge):
return self.response(account_key).gen_cert(key=kwargs.get('cert_key'))
@Challenge.register # pylint: disable=too-many-ancestors
class TLSALPN01(KeyAuthorizationChallenge):
"""ACME tls-alpn-01 challenge.
This class simply allows parsing the TLS-ALPN-01 challenge returned from
the CA. Full TLS-ALPN-01 support is not currently provided.
"""
typ = "tls-alpn-01"
def validation(self, account_key, **kwargs):
"""Generate validation for the challenge."""
raise NotImplementedError()
@Challenge.register # pylint: disable=too-many-ancestors
class DNS(_TokenChallenge):
"""ACME "dns" challenge."""

View File

@@ -393,38 +393,6 @@ class TLSSNI01Test(unittest.TestCase):
mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key)
class TLSALPN01Test(unittest.TestCase):
def setUp(self):
from acme.challenges import TLSALPN01
self.msg = TLSALPN01(
token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e'))
self.jmsg = {
'type': 'tls-alpn-01',
'token': 'a82d5ff8ef740d12881f6d3c2277ab2e',
}
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import TLSALPN01
self.assertEqual(self.msg, TLSALPN01.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import TLSALPN01
hash(TLSALPN01.from_json(self.jmsg))
def test_from_json_invalid_token_length(self):
from acme.challenges import TLSALPN01
self.jmsg['token'] = jose.encode_b64jose(b'abcd')
self.assertRaises(
jose.DeserializationError, TLSALPN01.from_json, self.jmsg)
def test_validation(self):
self.assertRaises(NotImplementedError, self.msg.validation, KEY)
class DNSTest(unittest.TestCase):
def setUp(self):

View File

@@ -42,38 +42,28 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
self.server_thread = threading.Thread(
# pylint: disable=no-member
target=self.server.handle_request)
self.server_thread.start()
time.sleep(1) # TODO: avoid race conditions in other way
def tearDown(self):
if self.server_thread.is_alive():
# The thread may have already terminated.
self.server_thread.join() # pragma: no cover
self.server_thread.join()
def _probe(self, name):
from acme.crypto_util import probe_sni
return jose.ComparableX509(probe_sni(
name, host='127.0.0.1', port=self.port))
def _start_server(self):
self.server_thread.start()
time.sleep(1) # TODO: avoid race conditions in other way
def test_probe_ok(self):
self._start_server()
self.assertEqual(self.cert, self._probe(b'foo'))
def test_probe_not_recognized_name(self):
self._start_server()
self.assertRaises(errors.Error, self._probe, b'bar')
def test_probe_connection_error(self):
# pylint has a hard time with six
self.server.server_close() # pylint: disable=no-member
original_timeout = socket.getdefaulttimeout()
try:
socket.setdefaulttimeout(1)
self.assertRaises(errors.Error, self._probe, b'bar')
finally:
socket.setdefaulttimeout(original_timeout)
# TODO: py33/py34 tox hangs forever on do_handshake in second probe
#def probe_connection_error(self):
# self._probe(b'foo')
# #time.sleep(1) # TODO: avoid race conditions in other way
# self.assertRaises(errors.Error, self._probe, b'bar')
class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase):

View File

@@ -4,10 +4,10 @@ import shutil
import socket
import threading
import tempfile
import time
import unittest
from six.moves import http_client # pylint: disable=import-error
from six.moves import queue # pylint: disable=import-error
from six.moves import socketserver # type: ignore # pylint: disable=import-error
import josepy as jose
@@ -16,6 +16,7 @@ import requests
from acme import challenges
from acme import crypto_util
from acme import errors
from acme import test_util
from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
@@ -260,9 +261,10 @@ class TestSimpleTLSSNI01Server(unittest.TestCase):
os.path.join(localhost_dir, 'key.pem'))
from acme.standalone import simple_tls_sni_01_server
self.port = 1234
self.thread = threading.Thread(
target=simple_tls_sni_01_server, kwargs={
'cli_args': ('filename',),
'cli_args': ('xxx', '--port', str(self.port)),
'forever': False,
},
)
@@ -274,20 +276,25 @@ class TestSimpleTLSSNI01Server(unittest.TestCase):
self.thread.join()
shutil.rmtree(self.test_cwd)
@mock.patch('acme.standalone.logger')
def test_it(self, mock_logger):
# Use a Queue because mock objects aren't thread safe.
q = queue.Queue() # type: queue.Queue[int]
# Add port number to the queue.
mock_logger.info.side_effect = lambda *args: q.put(args[-1])
self.thread.start()
def test_it(self):
max_attempts = 5
for attempt in range(max_attempts):
try:
cert = crypto_util.probe_sni(
b'localhost', b'0.0.0.0', self.port)
except errors.Error:
self.assertTrue(attempt + 1 < max_attempts, "Timeout!")
time.sleep(1) # wait until thread starts
else:
self.assertEqual(jose.ComparableX509(cert),
test_util.load_comparable_cert(
'rsa2048_cert.pem'))
break
# After the timeout, an exception is raised if the queue is empty.
port = q.get(timeout=5)
cert = crypto_util.probe_sni(b'localhost', b'0.0.0.0', port)
self.assertEqual(jose.ComparableX509(cert),
test_util.load_comparable_cert(
'rsa2048_cert.pem'))
if attempt == 0:
# the first attempt is always meant to fail, so we can test
# the socket failure code-path for probe_sni, as well
self.thread.start()
if __name__ == "__main__":

View File

@@ -1,2 +0,0 @@
[pytest]
norecursedirs = .* build dist CVS _darcs {arch} *.egg

View File

@@ -1,9 +1,10 @@
from setuptools import setup
from setuptools import find_packages
from setuptools.command.test import test as TestCommand
import sys
version = '0.26.0.dev0'
from setuptools import setup
from setuptools import find_packages
version = '0.25.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
@@ -34,19 +35,6 @@ docs_extras = [
'sphinx_rtd_theme',
]
class PyTest(TestCommand):
user_options = []
def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = ''
def run_tests(self):
import shlex
# import here, cause outside the eggs aren't loaded
import pytest
errno = pytest.main(shlex.split(self.pytest_args))
sys.exit(errno)
setup(
name='acme',
@@ -79,7 +67,5 @@ setup(
'dev': dev_extras,
'docs': docs_extras,
},
tests_require=["pytest"],
test_suite='acme',
cmdclass={"test": PyTest},
)

View File

@@ -1,5 +1,4 @@
""" Utility functions for certbot-apache plugin """
import binascii
import os
from certbot import util
@@ -99,8 +98,3 @@ def parse_define_file(filepath, varname):
var_parts = v[2:].partition("=")
return_vars[var_parts[0]] = var_parts[2]
return return_vars
def unique_id():
""" Returns an unique id to be used as a VirtualHost identifier"""
return binascii.hexlify(os.urandom(16)).decode("utf-8")

View File

@@ -13,7 +13,7 @@ import zope.component
import zope.interface
from acme import challenges
from acme.magic_typing import Any, DefaultDict, Dict, List, Set, Union # pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import DefaultDict, Dict, List, Set # pylint: disable=unused-import, no-name-in-module
from certbot import errors
from certbot import interfaces
@@ -22,7 +22,6 @@ from certbot import util
from certbot.achallenges import KeyAuthorizationAnnotatedChallenge # pylint: disable=unused-import
from certbot.plugins import common
from certbot.plugins.util import path_surgery
from certbot.plugins.enhancements import AutoHSTSEnhancement
from certbot_apache import apache_util
from certbot_apache import augeas_configurator
@@ -133,10 +132,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
default=cls.OS_DEFAULTS["challenge_location"],
help="Directory path for challenge configuration.")
add("handle-modules", default=cls.OS_DEFAULTS["handle_mods"],
help="Let installer handle enabling required modules for you. " +
help="Let installer handle enabling required modules for you." +
"(Only Ubuntu/Debian currently)")
add("handle-sites", default=cls.OS_DEFAULTS["handle_sites"],
help="Let installer handle enabling sites for you. " +
help="Let installer handle enabling sites for you." +
"(Only Ubuntu/Debian currently)")
util.add_deprecated_argument(add, argument_name="ctl", nargs=1)
util.add_deprecated_argument(
@@ -161,8 +160,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self._wildcard_vhosts = dict() # type: Dict[str, List[obj.VirtualHost]]
# Maps enhancements to vhosts we've enabled the enhancement for
self._enhanced_vhosts = defaultdict(set) # type: DefaultDict[str, Set[obj.VirtualHost]]
# Temporary state for AutoHSTS enhancement
self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]]
# These will be set in the prepare function
self.parser = None
@@ -1475,67 +1472,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if need_to_save:
self.save()
def find_vhost_by_id(self, id_str):
"""
Searches through VirtualHosts and tries to match the id in a comment
:param str id_str: Id string for matching
:returns: The matched VirtualHost or None
:rtype: :class:`~certbot_apache.obj.VirtualHost` or None
:raises .errors.PluginError: If no VirtualHost is found
"""
for vh in self.vhosts:
if self._find_vhost_id(vh) == id_str:
return vh
msg = "No VirtualHost with ID {} was found.".format(id_str)
logger.warning(msg)
raise errors.PluginError(msg)
def _find_vhost_id(self, vhost):
"""Tries to find the unique ID from the VirtualHost comments. This is
used for keeping track of VirtualHost directive over time.
:param vhost: Virtual host to add the id
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:returns: The unique ID or None
:rtype: str or None
"""
# Strip the {} off from the format string
search_comment = constants.MANAGED_COMMENT_ID.format("")
id_comment = self.parser.find_comments(search_comment, vhost.path)
if id_comment:
# Use the first value, multiple ones shouldn't exist
comment = self.parser.get_arg(id_comment[0])
return comment.split(" ")[-1]
return None
def add_vhost_id(self, vhost):
"""Adds an unique ID to the VirtualHost as a comment for mapping back
to it on later invocations, as the config file order might have changed.
If ID already exists, returns that instead.
:param vhost: Virtual host to add or find the id
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:returns: The unique ID for vhost
:rtype: str or None
"""
vh_id = self._find_vhost_id(vhost)
if vh_id:
return vh_id
id_string = apache_util.unique_id()
comment = constants.MANAGED_COMMENT_ID.format(id_string)
self.parser.add_comment(vhost.path, comment)
return id_string
def _escape(self, fp):
fp = fp.replace(",", "\\,")
fp = fp.replace("[", "\\[")
@@ -1595,78 +1531,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
logger.warning("Failed %s for %s", enhancement, domain)
raise
def _autohsts_increase(self, vhost, id_str, nextstep):
"""Increase the AutoHSTS max-age value
:param vhost: Virtual host object to modify
:type vhost: :class:`~certbot_apache.obj.VirtualHost`
:param str id_str: The unique ID string of VirtualHost
:param int nextstep: Next AutoHSTS max-age value index
"""
nextstep_value = constants.AUTOHSTS_STEPS[nextstep]
self._autohsts_write(vhost, nextstep_value)
self._autohsts[id_str] = {"laststep": nextstep, "timestamp": time.time()}
def _autohsts_write(self, vhost, nextstep_value):
"""
Write the new HSTS max-age value to the VirtualHost file
"""
hsts_dirpath = None
header_path = self.parser.find_dir("Header", None, vhost.path)
if header_path:
pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
for match in header_path:
if re.search(pat, self.aug.get(match).lower()):
hsts_dirpath = match
if not hsts_dirpath:
err_msg = ("Certbot was unable to find the existing HSTS header "
"from the VirtualHost at path {0}.").format(vhost.filep)
raise errors.PluginError(err_msg)
# Prepare the HSTS header value
hsts_maxage = "\"max-age={0}\"".format(nextstep_value)
# Update the header
# Our match statement was for string strict-transport-security, but
# we need to update the value instead. The next index is for the value
hsts_dirpath = hsts_dirpath.replace("arg[3]", "arg[4]")
self.aug.set(hsts_dirpath, hsts_maxage)
note_msg = ("Increasing HSTS max-age value to {0} for VirtualHost "
"in {1}\n".format(nextstep_value, vhost.filep))
logger.debug(note_msg)
self.save_notes += note_msg
self.save(note_msg)
def _autohsts_fetch_state(self):
"""
Populates the AutoHSTS state from the pluginstorage
"""
try:
self._autohsts = self.storage.fetch("autohsts")
except KeyError:
self._autohsts = dict()
def _autohsts_save_state(self):
"""
Saves the state of AutoHSTS object to pluginstorage
"""
self.storage.put("autohsts", self._autohsts)
self.storage.save()
def _autohsts_vhost_in_lineage(self, vhost, lineage):
"""
Searches AutoHSTS managed VirtualHosts that belong to the lineage.
Matches the private key path.
"""
return bool(
self.parser.find_dir("SSLCertificateKeyFile",
lineage.key_path, vhost.path))
def _enable_ocsp_stapling(self, ssl_vhost, unused_options):
"""Enables OCSP Stapling
@@ -2294,177 +2158,3 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# to be modified.
return common.install_version_controlled_file(options_ssl, options_ssl_digest,
self.constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES)
def enable_autohsts(self, _unused_lineage, domains):
"""
Enable the AutoHSTS enhancement for defined domains
:param _unused_lineage: Certificate lineage object, unused
:type _unused_lineage: certbot.storage.RenewableCert
:param domains: List of domains in certificate to enhance
:type domains: str
"""
self._autohsts_fetch_state()
_enhanced_vhosts = []
for d in domains:
matched_vhosts = self.choose_vhosts(d, create_if_no_ssl=False)
# We should be handling only SSL vhosts for AutoHSTS
vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]
if not vhosts:
msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
"domain {0} for enabling AutoHSTS enhancement.")
msg = msg_tmpl.format(d)
logger.warning(msg)
raise errors.PluginError(msg)
for vh in vhosts:
try:
self._enable_autohsts_domain(vh)
_enhanced_vhosts.append(vh)
except errors.PluginEnhancementAlreadyPresent:
if vh in _enhanced_vhosts:
continue
msg = ("VirtualHost for domain {0} in file {1} has a " +
"String-Transport-Security header present, exiting.")
raise errors.PluginEnhancementAlreadyPresent(
msg.format(d, vh.filep))
if _enhanced_vhosts:
note_msg = "Enabling AutoHSTS"
self.save(note_msg)
logger.info(note_msg)
self.restart()
# Save the current state to pluginstorage
self._autohsts_save_state()
def _enable_autohsts_domain(self, ssl_vhost):
"""Do the initial AutoHSTS deployment to a vhost
:param ssl_vhost: The VirtualHost object to deploy the AutoHSTS
:type ssl_vhost: :class:`~certbot_apache.obj.VirtualHost` or None
:raises errors.PluginEnhancementAlreadyPresent: When already enhanced
"""
# This raises the exception
self._verify_no_matching_http_header(ssl_vhost,
"Strict-Transport-Security")
if "headers_module" not in self.parser.modules:
self.enable_mod("headers")
# Prepare the HSTS header value
hsts_header = constants.HEADER_ARGS["Strict-Transport-Security"][:-1]
initial_maxage = constants.AUTOHSTS_STEPS[0]
hsts_header.append("\"max-age={0}\"".format(initial_maxage))
# Add ID to the VirtualHost for mapping back to it later
uniq_id = self.add_vhost_id(ssl_vhost)
self.save_notes += "Adding unique ID {0} to VirtualHost in {1}\n".format(
uniq_id, ssl_vhost.filep)
# Add the actual HSTS header
self.parser.add_dir(ssl_vhost.path, "Header", hsts_header)
note_msg = ("Adding gradually increasing HSTS header with initial value "
"of {0} to VirtualHost in {1}\n".format(
initial_maxage, ssl_vhost.filep))
self.save_notes += note_msg
# Save the current state to pluginstorage
self._autohsts[uniq_id] = {"laststep": 0, "timestamp": time.time()}
def update_autohsts(self, _unused_domain):
"""
Increase the AutoHSTS values of VirtualHosts that the user has enabled
this enhancement for.
:param _unused_domain: Not currently used
:type _unused_domain: Not Available
"""
self._autohsts_fetch_state()
if not self._autohsts:
# No AutoHSTS enabled for any domain
return
curtime = time.time()
save_and_restart = False
for id_str, config in list(self._autohsts.items()):
if config["timestamp"] + constants.AUTOHSTS_FREQ > curtime:
# Skip if last increase was < AUTOHSTS_FREQ ago
continue
nextstep = config["laststep"] + 1
if nextstep < len(constants.AUTOHSTS_STEPS):
# Have not reached the max value yet
try:
vhost = self.find_vhost_by_id(id_str)
except errors.PluginError:
msg = ("Could not find VirtualHost with ID {0}, disabling "
"AutoHSTS for this VirtualHost").format(id_str)
logger.warning(msg)
# Remove the orphaned AutoHSTS entry from pluginstorage
self._autohsts.pop(id_str)
continue
self._autohsts_increase(vhost, id_str, nextstep)
msg = ("Increasing HSTS max-age value for VirtualHost with id "
"{0}").format(id_str)
self.save_notes += msg
save_and_restart = True
if save_and_restart:
self.save("Increased HSTS max-age values")
self.restart()
self._autohsts_save_state()
def deploy_autohsts(self, lineage):
"""
Checks if autohsts vhost has reached maximum auto-increased value
and changes the HSTS max-age to a high value.
:param lineage: Certificate lineage object
:type lineage: certbot.storage.RenewableCert
"""
self._autohsts_fetch_state()
if not self._autohsts:
# No autohsts enabled for any vhost
return
vhosts = []
affected_ids = []
# Copy, as we are removing from the dict inside the loop
for id_str, config in list(self._autohsts.items()):
if config["laststep"]+1 >= len(constants.AUTOHSTS_STEPS):
# max value reached, try to make permanent
try:
vhost = self.find_vhost_by_id(id_str)
except errors.PluginError:
msg = ("VirtualHost with id {} was not found, unable to "
"make HSTS max-age permanent.").format(id_str)
logger.warning(msg)
self._autohsts.pop(id_str)
continue
if self._autohsts_vhost_in_lineage(vhost, lineage):
vhosts.append(vhost)
affected_ids.append(id_str)
save_and_restart = False
for vhost in vhosts:
self._autohsts_write(vhost, constants.AUTOHSTS_PERMANENT)
msg = ("Strict-Transport-Security max-age value for "
"VirtualHost in {0} was made permanent.").format(vhost.filep)
logger.debug(msg)
self.save_notes += msg+"\n"
save_and_restart = True
if save_and_restart:
self.save("Made HSTS max-age permanent")
self.restart()
for id_str in affected_ids:
self._autohsts.pop(id_str)
# Update AutoHSTS storage (We potentially removed vhosts from managed)
self._autohsts_save_state()
AutoHSTSEnhancement.register(ApacheConfigurator) # pylint: disable=no-member

View File

@@ -48,16 +48,3 @@ UIR_ARGS = ["always", "set", "Content-Security-Policy",
HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS,
"Upgrade-Insecure-Requests": UIR_ARGS}
AUTOHSTS_STEPS = [60, 300, 900, 3600, 21600, 43200, 86400]
"""AutoHSTS increase steps: 1min, 5min, 15min, 1h, 6h, 12h, 24h"""
AUTOHSTS_PERMANENT = 31536000
"""Value for the last max-age of HSTS"""
AUTOHSTS_FREQ = 172800
"""Minimum time since last increase to perform a new one: 48h"""
MANAGED_COMMENT = "DO NOT REMOVE - Managed by Certbot"
MANAGED_COMMENT_ID = MANAGED_COMMENT+", VirtualHost id: {0}"
"""Managed by Certbot comments and the VirtualHost identification template"""

View File

@@ -16,7 +16,6 @@ logger = logging.getLogger(__name__)
class ApacheParser(object):
# pylint: disable=too-many-public-methods
"""Class handles the fine details of parsing the Apache Configuration.
.. todo:: Make parsing general... remove sites-available etc...
@@ -351,37 +350,6 @@ class ApacheParser(object):
else:
self.aug.set(first_dir + "/arg", args)
def add_comment(self, aug_conf_path, comment):
"""Adds the comment to the augeas path
:param str aug_conf_path: Augeas configuration path to add directive
:param str comment: Comment content
"""
self.aug.set(aug_conf_path + "/#comment[last() + 1]", comment)
def find_comments(self, arg, start=None):
"""Finds a comment with specified content from the provided DOM path
:param str arg: Comment content to search
:param str start: Beginning Augeas path to begin looking
:returns: List of augeas paths containing the comment content
:rtype: list
"""
if not start:
start = get_aug_path(self.root)
comments = self.aug.match("%s//*[label() = '#comment']" % start)
results = []
for comment in comments:
c_content = self.aug.get(comment)
if c_content and arg in c_content:
results.append(comment)
return results
def find_dir(self, directive, arg=None, start=None, exclude=True):
"""Finds directive in the configuration.

View File

@@ -46,7 +46,6 @@ function Cleanup() {
# if our environment asks us to enable modules, do our best!
if [ "$1" = --debian-modules ] ; then
sudo apt-get install -y apache2
sudo apt-get install -y libapache2-mod-wsgi
sudo apt-get install -y libapache2-mod-macro

View File

@@ -1,181 +0,0 @@
# pylint: disable=too-many-public-methods,too-many-lines
"""Test for certbot_apache.configurator AutoHSTS functionality"""
import re
import unittest
import mock
# six is used in mock.patch()
import six # pylint: disable=unused-import
from certbot import errors
from certbot_apache import constants
from certbot_apache.tests import util
class AutoHSTSTest(util.ApacheTest):
"""Tests for AutoHSTS feature"""
# pylint: disable=protected-access
def setUp(self): # pylint: disable=arguments-differ
super(AutoHSTSTest, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
self.config.parser.modules.add("headers_module")
self.config.parser.modules.add("mod_headers.c")
self.config.parser.modules.add("ssl_module")
self.config.parser.modules.add("mod_ssl.c")
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
def get_autohsts_value(self, vh_path):
""" Get value from Strict-Transport-Security header """
header_path = self.config.parser.find_dir("Header", None, vh_path)
if header_path:
pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
for head in header_path:
if re.search(pat, self.config.parser.aug.get(head).lower()):
return self.config.parser.aug.get(head.replace("arg[3]",
"arg[4]"))
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
def test_autohsts_enable_headers_mod(self, mock_enable, _restart):
self.config.parser.modules.discard("headers_module")
self.config.parser.modules.discard("mod_header.c")
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
self.assertTrue(mock_enable.called)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
def test_autohsts_deploy_already_exists(self, _restart):
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
self.assertRaises(errors.PluginEnhancementAlreadyPresent,
self.config.enable_autohsts,
mock.MagicMock(), ["ocspvhost.com"])
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
def test_autohsts_increase(self, _mock_restart):
maxage = "\"max-age={0}\""
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
inc_val = maxage.format(constants.AUTOHSTS_STEPS[1])
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
# Verify initial value
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
initial_val)
# Increase
self.config.update_autohsts(mock.MagicMock())
# Verify increased value
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
inc_val)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache.configurator.ApacheConfigurator._autohsts_increase")
def test_autohsts_increase_noop(self, mock_increase, _restart):
maxage = "\"max-age={0}\""
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
# Verify initial value
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
initial_val)
self.config.update_autohsts(mock.MagicMock())
# Freq not patched, so value shouldn't increase
self.assertFalse(mock_increase.called)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
def test_autohsts_increase_no_header(self, _restart):
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
# Remove the header
dir_locs = self.config.parser.find_dir("Header", None,
self.vh_truth[7].path)
dir_loc = "/".join(dir_locs[0].split("/")[:-1])
self.config.parser.aug.remove(dir_loc)
self.assertRaises(errors.PluginError,
self.config.update_autohsts,
mock.MagicMock())
@mock.patch("certbot_apache.constants.AUTOHSTS_FREQ", 0)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
def test_autohsts_increase_and_make_permanent(self, _mock_restart):
maxage = "\"max-age={0}\""
max_val = maxage.format(constants.AUTOHSTS_PERMANENT)
mock_lineage = mock.MagicMock()
mock_lineage.key_path = "/etc/apache2/ssl/key-certbot_15.pem"
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
for i in range(len(constants.AUTOHSTS_STEPS)-1):
# Ensure that value is not made permanent prematurely
self.config.deploy_autohsts(mock_lineage)
self.assertNotEquals(self.get_autohsts_value(self.vh_truth[7].path),
max_val)
self.config.update_autohsts(mock.MagicMock())
# Value should match pre-permanent increment step
cur_val = maxage.format(constants.AUTOHSTS_STEPS[i+1])
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
cur_val)
# Make permanent
self.config.deploy_autohsts(mock_lineage)
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
max_val)
def test_autohsts_update_noop(self):
with mock.patch("time.time") as mock_time:
# Time mock is used to make sure that the execution does not
# continue when no autohsts entries exist in pluginstorage
self.config.update_autohsts(mock.MagicMock())
self.assertFalse(mock_time.called)
def test_autohsts_make_permanent_noop(self):
self.config.storage.put = mock.MagicMock()
self.config.deploy_autohsts(mock.MagicMock())
# Make sure that the execution does not continue when no entries in store
self.assertFalse(self.config.storage.put.called)
@mock.patch("certbot_apache.display_ops.select_vhost")
def test_autohsts_no_ssl_vhost(self, mock_select):
mock_select.return_value = self.vh_truth[0]
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
self.assertRaises(errors.PluginError,
self.config.enable_autohsts,
mock.MagicMock(), "invalid.example.com")
self.assertTrue(
"Certbot was not able to find SSL" in mock_log.call_args[0][0])
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
@mock.patch("certbot_apache.configurator.ApacheConfigurator.add_vhost_id")
def test_autohsts_dont_enhance_twice(self, mock_id, _restart):
mock_id.return_value = "1234567"
self.config.enable_autohsts(mock.MagicMock(),
["ocspvhost.com", "ocspvhost.com"])
self.assertEquals(mock_id.call_count, 1)
def test_autohsts_remove_orphaned(self):
# pylint: disable=protected-access
self.config._autohsts_fetch_state()
self.config._autohsts["orphan_id"] = {"laststep": 0, "timestamp": 0}
self.config._autohsts_save_state()
self.config.update_autohsts(mock.MagicMock())
self.assertFalse("orphan_id" in self.config._autohsts)
# Make sure it's removed from the pluginstorage file as well
self.config._autohsts = None
self.config._autohsts_fetch_state()
self.assertFalse(self.config._autohsts)
def test_autohsts_make_permanent_vhost_not_found(self):
# pylint: disable=protected-access
self.config._autohsts_fetch_state()
self.config._autohsts["orphan_id"] = {"laststep": 999, "timestamp": 0}
self.config._autohsts_save_state()
with mock.patch("certbot_apache.configurator.logger.warning") as mock_log:
self.config.deploy_autohsts(mock.MagicMock())
self.assertTrue(mock_log.called)
self.assertTrue(
"VirtualHost with id orphan_id was not" in mock_log.call_args[0][0])
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -1487,21 +1487,6 @@ class MultipleVhostsTest(util.ApacheTest):
"Upgrade-Insecure-Requests")
self.assertTrue(mock_choose.called)
def test_add_vhost_id(self):
for vh in [self.vh_truth[0], self.vh_truth[1], self.vh_truth[2]]:
vh_id = self.config.add_vhost_id(vh)
self.assertEqual(vh, self.config.find_vhost_by_id(vh_id))
def test_find_vhost_by_id_404(self):
self.assertRaises(errors.PluginError,
self.config.find_vhost_by_id,
"nonexistent")
def test_add_vhost_id_already_exists(self):
first_id = self.config.add_vhost_id(self.vh_truth[0])
second_id = self.config.add_vhost_id(self.vh_truth[0])
self.assertEqual(first_id, second_id)
class AugeasVhostsTest(util.ApacheTest):
"""Test vhosts with illegal names dependent on augeas version."""

View File

@@ -299,13 +299,6 @@ class BasicParserTest(util.ParserTest):
errors.MisconfigurationError,
self.parser.update_runtime_variables)
def test_add_comment(self):
from certbot_apache.parser import get_aug_path
self.parser.add_comment(get_aug_path(self.parser.loc["name"]), "123456")
comm = self.parser.find_comments("123456")
self.assertEquals(len(comm), 1)
self.assertTrue(self.parser.loc["name"] in comm[0])
class ParserInitTest(util.ApacheTest):
def setUp(self): # pylint: disable=arguments-differ

View File

@@ -1,2 +1,2 @@
acme[dev]==0.25.0
-e .[dev]
-e acme[dev]
certbot[dev]==0.21.1

View File

@@ -1,14 +1,16 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme>=0.25.0',
'certbot>=0.26.0.dev0',
'acme>0.24.0',
'certbot>=0.21.1',
'mock',
'python-augeas',
'setuptools',

View File

@@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
fi
VENV_BIN="$VENV_PATH/bin"
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
LE_AUTO_VERSION="0.25.1"
LE_AUTO_VERSION="0.24.0"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@@ -1055,11 +1055,9 @@ cffi==1.10.0 \
--hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \
--hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5
ConfigArgParse==0.12.0 \
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 \
--no-binary ConfigArgParse
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 \
--no-binary configobj
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
cryptography==2.0.2 \
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
@@ -1114,8 +1112,7 @@ mock==1.3.0 \
--hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \
--hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6
ordereddict==1.1 \
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f \
--no-binary ordereddict
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
packaging==16.8 \
--hash=sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388 \
--hash=sha256:5d50835fdf0a7edf0b55e311b7c887786504efea1177abd7e69329a8e5ea619e
@@ -1141,8 +1138,7 @@ pyRFC3339==1.0 \
--hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
python-augeas==0.5.0 \
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 \
--no-binary python-augeas
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
pytz==2015.7 \
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
@@ -1170,11 +1166,9 @@ unittest2==1.1.0 \
--hash=sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8 \
--hash=sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579
zope.component==4.2.2 \
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a \
--no-binary zope.component
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a
zope.event==4.1.0 \
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786 \
--no-binary zope.event
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786
zope.interface==4.1.3 \
--hash=sha256:f07b631f7a601cd8cbd3332d54f43142c7088a83299f859356f08d1d4d4259b3 \
--hash=sha256:de5cca083b9439d8002fb76bbe6b4998c5a5a721fab25b84298967f002df4c94 \
@@ -1193,9 +1187,6 @@ zope.interface==4.1.3 \
--hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \
--hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \
--hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392
requests-toolbelt==0.8.0 \
--hash=sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237 \
--hash=sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5
# Contains the requirements for the letsencrypt package.
#
@@ -1208,18 +1199,18 @@ letsencrypt==0.7.0 \
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
certbot==0.25.1 \
--hash=sha256:01689015364685fef3f1e1fb7832ba84eb3b0aa85bc5a71c96661f6d4c59981f \
--hash=sha256:5c23e5186133bb1afd805be5e0cd2fb7b95862a8b0459c9ecad4ae60f933e54e
acme==0.25.1 \
--hash=sha256:26e641a01536705fe5f12d856703b8ef06e5a07981a7b6379d2771dcdb69a742 \
--hash=sha256:47b5f3f73d69b7b1d13f918aa2cd75a8093069a68becf4af38e428e4613b2734
certbot-apache==0.25.1 \
--hash=sha256:a28b7c152cc11474bef5b5e7967aaea42b2c0aaf86fd82ee4082713d33cee5a9 \
--hash=sha256:ed012465617073a0f1057fe854dc8d1eb6d2dd7ede1fb2eee765129fed2a095a
certbot-nginx==0.25.1 \
--hash=sha256:83f82c3ba08c0b1d4bf449ac24018e8e7dd34a6248d35466f2de7da1cd312e15 \
--hash=sha256:68f98b41c54e0bf4218ef293079597176617bee3837ae3aa6528ce2ff0bf4f9c
certbot==0.24.0 \
--hash=sha256:a3fc41fde4f0dbb35f7ebec2f9e00339580b3f4298850411eac0719223073b27 \
--hash=sha256:a072d4528bb3ac4184f5c961a96931795ddfe4b7cb0f3a98954bdd4cce5f6d70
acme==0.24.0 \
--hash=sha256:b92b16102051f447abb52917638fbfb34b646ac07267fee85961b360a0149e32 \
--hash=sha256:d655e0627c0830114ab3f6732d8bf2f4a2c36f602e0cde10988684e229b501cb
certbot-apache==0.24.0 \
--hash=sha256:fe54db3e7e09ffe1139041c23ff5123e80aa1526d6fcd40b2a593d005cfcf152 \
--hash=sha256:686c6c0af5ae8d06e37cc762de7ffa0dc5c3b1ba06ff7653ef61713fa016f891
certbot-nginx==0.24.0 \
--hash=sha256:d44c419f72c2cc30de3b138a2cf92e0531696dcb048f287036e229dce2131c00 \
--hash=sha256:3283d1db057261f05537fa408baee20e0ab9e81c3d55cfba70afe3805cd6f941
UNLIKELY_EOF
# -------------------------------------------------------------------------

View File

@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
install_requires = [
'certbot',

View File

@@ -1,8 +1,10 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View File

@@ -1,8 +1,10 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View File

@@ -1,8 +1,10 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View File

@@ -1,8 +1,10 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View File

@@ -1,8 +1,10 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View File

@@ -1,8 +1,10 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View File

@@ -1,8 +1,10 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View File

@@ -1,8 +1,10 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View File

@@ -1,8 +1,10 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.

View File

@@ -1,2 +1,2 @@
acme[dev]==0.25.0
-e acme[dev]
certbot[dev]==0.21.1

View File

@@ -1,12 +1,14 @@
from setuptools import setup
import sys
from distutils.core import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme>=0.25.0',
'acme>0.24.0',
'certbot>=0.21.1',
'boto3',
'mock',

View File

@@ -69,9 +69,8 @@ class NginxConfigurator(common.Installer):
@classmethod
def add_parser_arguments(cls, add):
default_server_root = _determine_default_server_root()
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
help="Nginx server root directory. (default: %s)" % default_server_root)
help="Nginx server root directory.")
add("ctl", default=constants.CLI_DEFAULTS["ctl"], help="Path to the "
"'nginx' binary, used for 'configtest' and retrieving nginx "
"version number.")
@@ -290,8 +289,7 @@ class NginxConfigurator(common.Installer):
if not vhosts:
if create_if_no_match:
# result will not be [None] because it errors on failure
vhosts = [self._vhost_from_duplicated_default(target_name, True,
str(self.config.tls_sni_01_port))]
vhosts = [self._vhost_from_duplicated_default(target_name)]
else:
# No matches. Raise a misconfiguration error.
raise errors.MisconfigurationError(
@@ -334,12 +332,9 @@ class NginxConfigurator(common.Installer):
ipv6only_present = True
return (ipv6_active, ipv6only_present)
def _vhost_from_duplicated_default(self, domain, allow_port_mismatch, port):
"""if allow_port_mismatch is False, only server blocks with matching ports will be
used as a default server block template.
"""
def _vhost_from_duplicated_default(self, domain, port=None):
if self.new_vhost is None:
default_vhost = self._get_default_vhost(domain, allow_port_mismatch, port)
default_vhost = self._get_default_vhost(port, domain)
self.new_vhost = self.parser.duplicate_vhost(default_vhost,
remove_singleton_listen_params=True)
self.new_vhost.names = set()
@@ -355,24 +350,19 @@ class NginxConfigurator(common.Installer):
name_block[0].append(name)
self.parser.update_or_add_server_directives(vhost, name_block)
def _get_default_vhost(self, domain, allow_port_mismatch, port):
"""Helper method for _vhost_from_duplicated_default; see argument documentation there"""
def _get_default_vhost(self, port, domain):
vhost_list = self.parser.get_vhosts()
# if one has default_server set, return that one
all_default_vhosts = []
port_matching_vhosts = []
default_vhosts = []
for vhost in vhost_list:
for addr in vhost.addrs:
if addr.default:
all_default_vhosts.append(vhost)
if self._port_matches(port, addr.get_port()):
port_matching_vhosts.append(vhost)
break
if port is None or self._port_matches(port, addr.get_port()):
default_vhosts.append(vhost)
break
if len(port_matching_vhosts) == 1:
return port_matching_vhosts[0]
elif len(all_default_vhosts) == 1 and allow_port_mismatch:
return all_default_vhosts[0]
if len(default_vhosts) == 1:
return default_vhosts[0]
# TODO: present a list of vhosts for user to choose from
@@ -481,7 +471,7 @@ class NginxConfigurator(common.Installer):
matches = self._get_redirect_ranked_matches(target_name, port)
vhosts = [x for x in [self._select_best_name_match(matches)]if x is not None]
if not vhosts and create_if_no_match:
vhosts = [self._vhost_from_duplicated_default(target_name, False, port)]
vhosts = [self._vhost_from_duplicated_default(target_name, port=port)]
return vhosts
def _port_matches(self, test_port, matching_port):
@@ -1130,11 +1120,3 @@ def install_ssl_options_conf(options_ssl, options_ssl_digest):
"""Copy Certbot's SSL options file into the system's config dir if required."""
return common.install_version_controlled_file(options_ssl, options_ssl_digest,
constants.MOD_SSL_CONF_SRC, constants.ALL_SSL_OPTIONS_HASHES)
def _determine_default_server_root():
if os.environ.get("CERTBOT_DOCS") == "1":
default_server_root = "%s or %s" % (constants.LINUX_SERVER_ROOT,
constants.FREEBSD_DARWIN_SERVER_ROOT)
else:
default_server_root = constants.CLI_DEFAULTS["server_root"]
return default_server_root

View File

@@ -2,13 +2,10 @@
import pkg_resources
import platform
FREEBSD_DARWIN_SERVER_ROOT = "/usr/local/etc/nginx"
LINUX_SERVER_ROOT = "/etc/nginx"
if platform.system() in ('FreeBSD', 'Darwin'):
server_root_tmp = FREEBSD_DARWIN_SERVER_ROOT
server_root_tmp = "/usr/local/etc/nginx"
else:
server_root_tmp = LINUX_SERVER_ROOT
server_root_tmp = "/etc/nginx"
CLI_DEFAULTS = dict(
server_root=server_root_tmp,

View File

@@ -566,7 +566,7 @@ def _update_or_add_directives(directives, insert_at_top, block):
INCLUDE = 'include'
REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE, 'rewrite', 'add_header'])
REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE, 'rewrite'])
COMMENT = ' managed by Certbot'
COMMENT_BLOCK = [' ', '#', COMMENT]

View File

@@ -47,7 +47,7 @@ class NginxConfiguratorTest(util.NginxTest):
def test_prepare(self):
self.assertEqual((1, 6, 2), self.config.version)
self.assertEqual(11, len(self.config.parser.parsed))
self.assertEqual(10, len(self.config.parser.parsed))
@mock.patch("certbot_nginx.configurator.util.exe_exists")
@mock.patch("certbot_nginx.configurator.subprocess.Popen")
@@ -91,8 +91,7 @@ class NginxConfiguratorTest(util.NginxTest):
self.assertEqual(names, set(
["155.225.50.69.nephoscale.net", "www.example.org", "another.alias",
"migration.com", "summer.com", "geese.com", "sslon.com",
"globalssl.com", "globalsslsetssl.com", "ipv6.com", "ipv6ssl.com",
"headers.com"]))
"globalssl.com", "globalsslsetssl.com", "ipv6.com", "ipv6ssl.com"]))
def test_supported_enhancements(self):
self.assertEqual(['redirect', 'ensure-http-header', 'staple-ocsp'],
@@ -549,14 +548,6 @@ class NginxConfiguratorTest(util.NginxTest):
generated_conf = self.config.parser.parsed[example_conf]
self.assertTrue(util.contains_at_depth(generated_conf, expected, 2))
def test_multiple_headers_hsts(self):
headers_conf = self.config.parser.abs_path('sites-enabled/headers.com')
self.config.enhance("headers.com", "ensure-http-header",
"Strict-Transport-Security")
expected = ['add_header', 'Strict-Transport-Security', '"max-age=31536000"', 'always']
generated_conf = self.config.parser.parsed[headers_conf]
self.assertTrue(util.contains_at_depth(generated_conf, expected, 2))
def test_http_header_hsts_twice(self):
self.config.enhance("www.example.com", "ensure-http-header",
"Strict-Transport-Security")
@@ -731,13 +722,6 @@ class NginxConfiguratorTest(util.NginxTest):
"www.nomatch.com", "example/cert.pem", "example/key.pem",
"example/chain.pem", "example/fullchain.pem")
def test_deploy_no_match_multiple_defaults_ok(self):
foo_conf = self.config.parser.abs_path('foo.conf')
self.config.parser.parsed[foo_conf][2][1][0][1][0][1] = '*:5001'
self.config.version = (1, 3, 1)
self.config.deploy_cert("www.nomatch.com", "example/cert.pem", "example/key.pem",
"example/chain.pem", "example/fullchain.pem")
def test_deploy_no_match_add_redirect(self):
default_conf = self.config.parser.abs_path('sites-enabled/default')
foo_conf = self.config.parser.abs_path('foo.conf')
@@ -868,7 +852,7 @@ class NginxConfiguratorTest(util.NginxTest):
prefer_ssl=False,
no_ssl_filter_port='80')
# Check that the dialog was called with only port 80 vhosts
self.assertEqual(len(mock_select_vhs.call_args[0][0]), 5)
self.assertEqual(len(mock_select_vhs.call_args[0][0]), 4)
class InstallSslOptionsConfTest(util.NginxTest):
@@ -949,30 +933,5 @@ class InstallSslOptionsConfTest(util.NginxTest):
" with the sha256 hash of self.config.mod_ssl_conf when it is updated.")
class DetermineDefaultServerRootTest(certbot_test_util.ConfigTestCase):
"""Tests for certbot_nginx.configurator._determine_default_server_root."""
def _call(self):
from certbot_nginx.configurator import _determine_default_server_root
return _determine_default_server_root()
@mock.patch.dict(os.environ, {"CERTBOT_DOCS": "1"})
def test_docs_value(self):
self._test(expect_both_values=True)
@mock.patch.dict(os.environ, {})
def test_real_values(self):
self._test(expect_both_values=False)
def _test(self, expect_both_values):
server_root = self._call()
if expect_both_values:
self.assertIn("/usr/local/etc/nginx", server_root)
self.assertIn("/etc/nginx", server_root)
else:
self.assertTrue(server_root == "/etc/nginx" or server_root == "/usr/local/etc/nginx")
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -49,7 +49,6 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
['foo.conf', 'nginx.conf', 'server.conf',
'sites-enabled/default',
'sites-enabled/example.com',
'sites-enabled/headers.com',
'sites-enabled/migration.com',
'sites-enabled/sslon.com',
'sites-enabled/globalssl.com',
@@ -78,7 +77,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
parsed = nparser._parse_files(nparser.abs_path(
'sites-enabled/example.com.test'))
self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test'))))
self.assertEqual(8, len(
self.assertEqual(7, len(
glob.glob(nparser.abs_path('sites-enabled/*.test'))))
self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'],
@@ -161,7 +160,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
'*.www.example.com']),
[], [2, 1, 0])
self.assertEqual(13, len(vhosts))
self.assertEqual(12, len(vhosts))
example_com = [x for x in vhosts if 'example.com' in x.filep][0]
self.assertEqual(vhost3, example_com)
default = [x for x in vhosts if 'default' in x.filep][0]

View File

@@ -1,4 +0,0 @@
server {
server_name headers.com;
add_header X-Content-Type-Options nosniff;
}

View File

@@ -1,2 +1,2 @@
acme[dev]==0.25.0
-e acme[dev]
-e .[dev]

View File

@@ -1,14 +1,19 @@
import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.25.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme>=0.25.0',
'certbot>=0.22.0',
# This plugin works with an older version of acme, but Certbot does not.
# 0.22.0 is specified here to work around
# https://github.com/pypa/pip/issues/988.
'acme>0.21.1',
'certbot>0.21.1',
'mock',
'PyOpenSSL',
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?

View File

@@ -62,5 +62,3 @@ test_deployment_and_rollback nginx6.wtf
# note: not reached if anything above fails, hence "killall" at the
# top
nginx -c $nginx_root/nginx.conf -s stop
coverage report --fail-under 75 --include 'certbot-nginx/*' --show-missing

View File

@@ -1,190 +0,0 @@
Copyright 2017 Electronic Frontier Foundation and others
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -1,4 +0,0 @@
include LICENSE.txt
include README.rst
recursive-include certbot_postfix/testdata *
recursive-include certbot_postfix/docs *

View File

@@ -1,23 +0,0 @@
==========================
Postfix plugin for Certbot
==========================
Note: this MTA installer is in **developer beta**-- we appreciate any testing, feedback, or
feature requests for this plugin.
To install this plugin, in the root of this repo, run::
./tools/venv.sh
source venv/bin/activate
You can use this installer with any `authenticator plugin
<https://certbot.eff.org/docs/using.html#getting-certificates-and-choosing-plugins>`_.
For instance, with the `standalone authenticator
<https://certbot.eff.org/docs/using.html#standalone>`_, which requires no extra server
software, you might run::
sudo ./venv/bin/certbot run --standalone -i postfix -d <domain name>
To just install existing certs with this plugin, run::
sudo ./venv/bin/certbot install -i postfix --cert-path <path to cert> --key-path <path to key> -d <domain name>

View File

@@ -1,3 +0,0 @@
"""Certbot Postfix plugin."""
from certbot_postfix.installer import Installer

View File

@@ -1,63 +0,0 @@
"""Postfix plugin constants."""
# pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Dict, Tuple, Union
# pylint: enable=unused-import, no-name-in-module
MINIMUM_VERSION = (2, 11,)
# If the value of a default VAR is a tuple, then the values which
# come LATER in the tuple are more strict/more secure.
# Certbot will default to the first value in the tuple, but will
# not override "more secure" settings.
ACCEPTABLE_SERVER_SECURITY_LEVELS = ("may", "encrypt")
ACCEPTABLE_CLIENT_SECURITY_LEVELS = ("may", "encrypt",
"dane", "dane-only",
"fingerprint",
"verify", "secure")
ACCEPTABLE_CIPHER_LEVELS = ("medium", "high")
# Exporting certain ciphers to prevent logjam: https://weakdh.org/sysadmin.html
EXCLUDE_CIPHERS = ("aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, "
"EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA",)
TLS_VERSIONS = ("SSLv2", "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2")
# Should NOT use SSLv2/3.
ACCEPTABLE_TLS_VERSIONS = ("TLSv1", "TLSv1.1", "TLSv1.2")
# Variables associated with enabling opportunistic TLS.
TLS_SERVER_VARS = {
"smtpd_tls_security_level": ACCEPTABLE_SERVER_SECURITY_LEVELS,
} # type:Dict[str, Tuple[str, ...]]
TLS_CLIENT_VARS = {
"smtp_tls_security_level": ACCEPTABLE_CLIENT_SECURITY_LEVELS,
} # type:Dict[str, Tuple[str, ...]]
# Default variables for a secure MTA server [receiver].
DEFAULT_SERVER_VARS = {
"smtpd_tls_auth_only": ("yes",),
"smtpd_tls_mandatory_protocols": ("!SSLv2, !SSLv3",),
"smtpd_tls_protocols": ("!SSLv2, !SSLv3",),
"smtpd_tls_ciphers": ACCEPTABLE_CIPHER_LEVELS,
"smtpd_tls_mandatory_ciphers": ACCEPTABLE_CIPHER_LEVELS,
"smtpd_tls_exclude_ciphers": EXCLUDE_CIPHERS,
"smtpd_tls_eecdh_grade": ("strong",),
} # type:Dict[str, Tuple[str, ...]]
# Default variables for a secure MTA client [sender].
DEFAULT_CLIENT_VARS = {
"smtp_tls_ciphers": ACCEPTABLE_CIPHER_LEVELS,
"smtp_tls_exclude_ciphers": EXCLUDE_CIPHERS,
"smtp_tls_mandatory_ciphers": ACCEPTABLE_CIPHER_LEVELS,
} # type:Dict[str, Tuple[str, ...]]
CLI_DEFAULTS = dict(
config_dir="/etc/postfix",
ctl="postfix",
config_utility="postconf",
tls_only=False,
ignore_master_overrides=False,
server_only=False,
)
"""CLI defaults."""

View File

@@ -1,288 +0,0 @@
"""certbot installer plugin for postfix."""
import logging
import os
import zope.interface
import zope.component
import six
from certbot import errors
from certbot import interfaces
from certbot import util as certbot_util
from certbot.plugins import common as plugins_common
from certbot_postfix import constants
from certbot_postfix import postconf
from certbot_postfix import util
# pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Callable, Dict, List
# pylint: enable=unused-import, no-name-in-module
logger = logging.getLogger(__name__)
@zope.interface.implementer(interfaces.IInstaller)
@zope.interface.provider(interfaces.IPluginFactory)
class Installer(plugins_common.Installer):
"""Certbot installer plugin for Postfix.
:ivar str config_dir: Postfix configuration directory to modify
:ivar list save_notes: documentation for proposed changes. This is
cleared and stored in Certbot checkpoints when save() is called
:ivar postconf: Wrapper for Postfix configuration command-line tool.
:type postconf: :class: `certbot_postfix.postconf.ConfigMain`
:ivar postfix: Wrapper for Postfix command-line tool.
:type postfix: :class: `certbot_postfix.util.PostfixUtil`
"""
description = "Configure TLS with the Postfix MTA"
@classmethod
def add_parser_arguments(cls, add):
add("ctl", default=constants.CLI_DEFAULTS["ctl"],
help="Path to the 'postfix' control program.")
# This directory points to Postfix's configuration directory.
add("config-dir", default=constants.CLI_DEFAULTS["config_dir"],
help="Path to the directory containing the "
"Postfix main.cf file to modify instead of using the "
"default configuration paths.")
add("config-utility", default=constants.CLI_DEFAULTS["config_utility"],
help="Path to the 'postconf' executable.")
add("tls-only", action="store_true", default=constants.CLI_DEFAULTS["tls_only"],
help="Only set params to enable opportunistic TLS and install certificates.")
add("server-only", action="store_true", default=constants.CLI_DEFAULTS["server_only"],
help="Only set server params (prefixed with smtpd*)")
add("ignore-master-overrides", action="store_true",
default=constants.CLI_DEFAULTS["ignore_master_overrides"],
help="Ignore errors reporting overridden TLS parameters in master.cf.")
def __init__(self, *args, **kwargs):
super(Installer, self).__init__(*args, **kwargs)
# Wrapper around postconf commands
self.postfix = None
self.postconf = None
# Files to save
self.save_notes = [] # type: List[str]
self._enhance_func = {} # type: Dict[str, Callable[[str, str], None]]
# Since we only need to enable TLS once for all domains,
# keep track of whether this enhancement was already called.
self._tls_enabled = False
def prepare(self):
"""Prepare the installer.
:raises errors.PluginError: when an unexpected error occurs
:raises errors.MisconfigurationError: when the config is invalid
:raises errors.NoInstallationError: when can't find installation
:raises errors.NotSupportedError: when version is not supported
"""
# Verify postfix and postconf are installed
for param in ("ctl", "config_utility",):
util.verify_exe_exists(self.conf(param),
"Cannot find executable '{0}'. You can provide the "
"path to this command with --{1}".format(
self.conf(param),
self.option_name(param)))
# Set up CLI tools
self.postfix = util.PostfixUtil(self.conf('config-dir'))
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
self.conf('ignore-master-overrides'),
self.conf('config-dir'))
# Ensure current configuration is valid.
self.config_test()
# Check Postfix version
self._check_version()
self._lock_config_dir()
self.install_ssl_dhparams()
def config_test(self):
"""Test to see that the current Postfix configuration is valid.
:raises errors.MisconfigurationError: If the configuration is invalid.
"""
self.postfix.test()
def _check_version(self):
"""Verifies that the installed Postfix version is supported.
:raises errors.NotSupportedError: if the version is unsupported
"""
if self._get_version() < constants.MINIMUM_VERSION:
version_string = '.'.join([str(n) for n in constants.MINIMUM_VERSION])
raise errors.NotSupportedError('Postfix version must be at least %s' % version_string)
def _lock_config_dir(self):
"""Stop two Postfix plugins from modifying the config at once.
:raises .PluginError: if unable to acquire the lock
"""
try:
certbot_util.lock_dir_until_exit(self.conf('config-dir'))
except (OSError, errors.LockError):
logger.debug("Encountered error:", exc_info=True)
raise errors.PluginError(
"Unable to lock %s" % self.conf('config-dir'))
def more_info(self):
"""Human-readable string to help the user. Describes steps taken and any relevant
info to help the user decide which plugin to use.
:rtype: str
"""
return (
"Configures Postfix to try to authenticate mail servers, use "
"installed certificates and disable weak ciphers and protocols.{0}"
"Server root: {root}{0}"
"Version: {version}".format(
os.linesep,
root=self.conf('config-dir'),
version='.'.join([str(i) for i in self._get_version()]))
)
def _get_version(self):
"""Return the version of Postfix, as a tuple. (e.g. '2.11.3' is (2, 11, 3))
:returns: version
:rtype: tuple
:raises errors.PluginError: Unable to find Postfix version.
"""
mail_version = self.postconf.get_default("mail_version")
return tuple(int(i) for i in mail_version.split('.'))
def get_all_names(self):
"""Returns all names that may be authenticated.
:rtype: `set` of `str`
"""
return certbot_util.get_filtered_names(self.postconf.get(var)
for var in ('mydomain', 'myhostname', 'myorigin',))
def _set_vars(self, var_dict):
"""Sets all parameters in var_dict to config file. If current value is already set
as more secure (acceptable), then don't set/overwrite it.
"""
for param, acceptable in six.iteritems(var_dict):
if not util.is_acceptable_value(param, self.postconf.get(param), acceptable):
self.postconf.set(param, acceptable[0], acceptable)
def _confirm_changes(self):
"""Confirming outstanding updates for configuration parameters.
:raises errors.PluginError: when user rejects the configuration changes.
"""
updates = self.postconf.get_changes()
output_string = "Postfix TLS configuration parameters to update in main.cf:\n"
for name, value in six.iteritems(updates):
output_string += "{0} = {1}\n".format(name, value)
output_string += "Is this okay?\n"
if not zope.component.getUtility(interfaces.IDisplay).yesno(output_string,
force_interactive=True, default=True):
raise errors.PluginError(
"Manually rejected configuration changes.\n"
"Try using --tls-only or --server-only to change a particular"
"subset of configuration parameters.")
def deploy_cert(self, domain, cert_path,
key_path, chain_path, fullchain_path):
"""Configure the Postfix SMTP server to use the given TLS cert.
:param str domain: domain to deploy certificate file
:param str cert_path: absolute path to the certificate file
:param str key_path: absolute path to the private key file
:param str chain_path: absolute path to the certificate chain file
:param str fullchain_path: absolute path to the certificate fullchain
file (cert plus chain)
:raises .PluginError: when cert cannot be deployed
"""
# pylint: disable=unused-argument
if self._tls_enabled:
return
self._tls_enabled = True
self.save_notes.append("Configuring TLS for {0}".format(domain))
self.postconf.set("smtpd_tls_cert_file", cert_path)
self.postconf.set("smtpd_tls_key_file", key_path)
self._set_vars(constants.TLS_SERVER_VARS)
if not self.conf('server_only'):
self._set_vars(constants.TLS_CLIENT_VARS)
if not self.conf('tls_only'):
self._set_vars(constants.DEFAULT_SERVER_VARS)
if not self.conf('server_only'):
self._set_vars(constants.DEFAULT_CLIENT_VARS)
# Despite the name, this option also supports 2048-bit DH params.
# http://www.postfix.org/FORWARD_SECRECY_README.html#server_fs
self.postconf.set("smtpd_tls_dh1024_param_file", self.ssl_dhparams)
self._confirm_changes()
def enhance(self, domain, enhancement, options=None):
"""Raises an exception since this installer doesn't support any enhancements.
"""
# pylint: disable=unused-argument
raise errors.PluginError(
"Unsupported enhancement: {0}".format(enhancement))
def supported_enhancements(self):
"""Returns a list of supported enhancements.
:rtype: list
"""
return []
def save(self, title=None, temporary=False):
"""Creates backups and writes changes to configuration files.
:param str title: The title of the save. If a title is given, the
configuration will be saved as a new checkpoint and put in a
timestamped directory. `title` has no effect if temporary is true.
:param bool temporary: Indicates whether the changes made will
be quickly reversed in the future (challenges)
:raises errors.PluginError: when save is unsuccessful
"""
save_files = set((os.path.join(self.conf('config-dir'), "main.cf"),))
self.add_to_checkpoint(save_files,
"\n".join(self.save_notes), temporary)
self.postconf.flush()
del self.save_notes[:]
if title and not temporary:
self.finalize_checkpoint(title)
def recovery_routine(self):
super(Installer, self).recovery_routine()
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
self.conf('ignore-master-overrides'),
self.conf('config-dir'))
def rollback_checkpoints(self, rollback=1):
"""Rollback saved checkpoints.
:param int rollback: Number of checkpoints to revert
:raises .errors.PluginError: If there is a problem with the input or
the function is unable to correctly revert the configuration
"""
super(Installer, self).rollback_checkpoints(rollback)
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
self.conf('ignore-master-overrides'),
self.conf('config-dir'))
def restart(self):
"""Restart or refresh the server content.
:raises .PluginError: when server cannot be restarted
"""
self.postfix.restart()

View File

@@ -1,152 +0,0 @@
"""Classes that wrap the postconf command line utility.
"""
import six
from certbot import errors
from certbot_postfix import util
# pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Dict, List, Tuple
# pylint: enable=unused-import, no-name-in-module
class ConfigMain(util.PostfixUtilBase):
"""A parser for Postfix's main.cf file."""
def __init__(self, executable, ignore_master_overrides=False, config_dir=None):
super(ConfigMain, self).__init__(executable, config_dir)
# Whether to ignore overrides from master.
self._ignore_master_overrides = ignore_master_overrides
# List of all current Postfix parameters, from `postconf` command.
self._db = {} # type: Dict[str, str]
# List of current master.cf overrides from Postfix config. Dictionary
# of parameter name => list of tuples (service name, paramter value)
# Note: We should never modify master without explicit permission.
self._master_db = {} # type: Dict[str, List[Tuple[str, str]]]
# List of all changes requested to the Postfix parameters as they are now
# in _db. These changes are flushed to `postconf` on `flush`.
self._updated = {} # type: Dict[str, str]
self._read_from_conf()
def _read_from_conf(self):
"""Reads initial parameter state from `main.cf` into this object.
"""
out = self._get_output()
for name, value in _parse_main_output(out):
self._db[name] = value
out = self._get_output_master()
for name, value in _parse_main_output(out):
service, param_name = name.rsplit("/", 1)
if param_name not in self._master_db:
self._master_db[param_name] = []
self._master_db[param_name].append((service, value))
def _get_output_master(self):
"""Retrieves output for `master.cf` parameters."""
return self._get_output('-P')
def get_default(self, name):
"""Retrieves default value of parameter `name` from postfix parameters.
:param str name: The name of the parameter to fetch.
:returns: The default value of parameter `name`.
:rtype: str
"""
out = self._get_output(['-d', name])
_, value = next(_parse_main_output(out), (None, None))
return value
def get(self, name):
"""Retrieves working value of parameter `name` from postfix parameters.
:param str name: The name of the parameter to fetch.
:returns: The value of parameter `name`.
:rtype: str
"""
if name in self._updated:
return self._updated[name]
return self._db[name]
def get_master_overrides(self, name):
"""Retrieves list of overrides for parameter `name` in postfix's Master config
file.
:returns: List of tuples (service, value), meaning that parameter `name`
is overridden as `value` for `service`.
:rtype: `list` of `tuple` of `str`
"""
if name in self._master_db:
return self._master_db[name]
return None
def set(self, name, value, acceptable_overrides=None):
"""Sets parameter `name` to `value`. If `name` is overridden by a particular service in
`master.cf`, reports any of these parameter conflicts as long as
`ignore_master_overrides` was not set.
.. note:: that this function does not flush these parameter values to main.cf;
To do that, use `flush`.
:param str name: The name of the parameter to set.
:param str value: The value of the parameter.
:param tuple acceptable_overrides: If the master configuration file overrides `value`
with a value in acceptable_overrides.
"""
if name not in self._db:
raise KeyError("Parameter name %s is not a valid Postfix parameter name.", name)
# Check to see if this parameter is overridden by master.
overrides = self.get_master_overrides(name)
if not self._ignore_master_overrides and overrides is not None:
util.report_master_overrides(name, overrides, acceptable_overrides)
if value != self._db[name]:
# _db contains the "original" state of parameters. We only care about
# writes if they cause a delta from the original state.
self._updated[name] = value
elif name in self._updated:
# If this write reverts a previously updated parameter back to the
# original DB's state, we don't have to keep track of it in _updated.
del self._updated[name]
def flush(self):
"""Flushes all parameter changes made using `self.set`, to `main.cf`
:raises error.PluginError: When flush to main.cf fails for some reason.
"""
if len(self._updated) == 0:
return
args = ['-e']
for name, value in six.iteritems(self._updated):
args.append('{0}={1}'.format(name, value))
try:
self._get_output(args)
except IOError as e:
raise errors.PluginError("Unable to save to Postfix config: %v", e)
for name, value in six.iteritems(self._updated):
self._db[name] = value
self._updated = {}
def get_changes(self):
""" Return queued changes to main.cf.
:rtype: dict[str, str]
"""
return self._updated
def _parse_main_output(output):
"""Parses the raw output from Postconf about main.cf.
Expects the output to look like:
.. code-block:: none
name1 = value1
name2 = value2
:param str output: data postconf wrote to stdout about main.cf
:returns: generator providing key-value pairs from main.cf
:rtype: Iterator[tuple(str, str)]
"""
for line in output.splitlines():
name, _, value = line.partition(" =")
yield name, value.strip()

View File

@@ -1 +0,0 @@
""" Certbot Postfix Tests """

View File

@@ -1,314 +0,0 @@
"""Tests for certbot_postfix.installer."""
from contextlib import contextmanager
import copy
import functools
import os
import pkg_resources
import six
import unittest
import mock
from certbot import errors
from certbot.tests import util as certbot_test_util
# pylint: disable=unused-import, no-name-in-module
from acme.magic_typing import Dict, Tuple, Union
# pylint: enable=unused-import, no-name-in-module
DEFAULT_MAIN_CF = {
"smtpd_tls_cert_file": "",
"smtpd_tls_key_file": "",
"smtpd_tls_dh1024_param_file": "",
"smtpd_tls_security_level": "none",
"smtpd_tls_auth_only": "",
"smtpd_tls_mandatory_protocols": "",
"smtpd_tls_protocols": "",
"smtpd_tls_ciphers": "",
"smtpd_tls_exclude_ciphers": "",
"smtpd_tls_mandatory_ciphers": "",
"smtpd_tls_eecdh_grade": "medium",
"smtp_tls_security_level": "",
"smtp_tls_ciphers": "",
"smtp_tls_exclude_ciphers": "",
"smtp_tls_mandatory_ciphers": "",
"mail_version": "3.2.3"
}
def _main_cf_with(obj):
main_cf = copy.copy(DEFAULT_MAIN_CF)
main_cf.update(obj)
return main_cf
class InstallerTest(certbot_test_util.ConfigTestCase):
# pylint: disable=too-many-public-methods
def setUp(self):
super(InstallerTest, self).setUp()
_config_file = pkg_resources.resource_filename("certbot_postfix.tests",
os.path.join("testdata", "config.json"))
self.config.postfix_ctl = "postfix"
self.config.postfix_config_dir = self.tempdir
self.config.postfix_config_utility = "postconf"
self.config.postfix_tls_only = False
self.config.postfix_server_only = False
self.config.config_dir = self.tempdir
@mock.patch("certbot_postfix.installer.util.is_acceptable_value")
def test_set_vars(self, mock_is_acceptable_value):
mock_is_acceptable_value.return_value = True
with create_installer(self.config) as installer:
installer.prepare()
mock_is_acceptable_value.return_value = False
@mock.patch("certbot_postfix.installer.util.is_acceptable_value")
def test_acceptable_value(self, mock_is_acceptable_value):
mock_is_acceptable_value.return_value = True
with create_installer(self.config) as installer:
installer.prepare()
mock_is_acceptable_value.return_value = False
@certbot_test_util.patch_get_utility()
def test_confirm_changes_no_raises_error(self, mock_util):
mock_util().yesno.return_value = False
with create_installer(self.config) as installer:
installer.prepare()
self.assertRaises(errors.PluginError, installer.deploy_cert,
"example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
@certbot_test_util.patch_get_utility()
def test_save(self, mock_util):
mock_util().yesno.return_value = True
with create_installer(self.config) as installer:
installer.prepare()
installer.postconf.flush = mock.Mock()
installer.reverter = mock.Mock()
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
installer.save()
self.assertEqual(installer.save_notes, [])
self.assertEqual(installer.postconf.flush.call_count, 1)
self.assertEqual(installer.reverter.add_to_checkpoint.call_count, 1)
@certbot_test_util.patch_get_utility()
def test_save_with_title(self, mock_util):
mock_util().yesno.return_value = True
with create_installer(self.config) as installer:
installer.prepare()
installer.postconf.flush = mock.Mock()
installer.reverter = mock.Mock()
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
installer.save(title="new_file!")
self.assertEqual(installer.reverter.finalize_checkpoint.call_count, 1)
@certbot_test_util.patch_get_utility()
def test_rollback_checkpoints_resets_postconf(self, mock_util):
mock_util().yesno.return_value = True
with create_installer(self.config) as installer:
installer.prepare()
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
installer.rollback_checkpoints()
self.assertEqual(installer.postconf.get_changes(), {})
@certbot_test_util.patch_get_utility()
def test_recovery_routine_resets_postconf(self, mock_util):
mock_util().yesno.return_value = True
with create_installer(self.config) as installer:
installer.prepare()
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
installer.recovery_routine()
self.assertEqual(installer.postconf.get_changes(), {})
def test_restart(self):
with create_installer(self.config) as installer:
installer.prepare()
installer.restart()
self.assertEqual(installer.postfix.restart.call_count, 1)
def test_add_parser_arguments(self):
options = set(("ctl", "config-dir", "config-utility",
"tls-only", "server-only", "ignore-master-overrides"))
mock_add = mock.MagicMock()
from certbot_postfix import installer
installer.Installer.add_parser_arguments(mock_add)
for call in mock_add.call_args_list:
self.assertTrue(call[0][0] in options)
def test_no_postconf_prepare(self):
with create_installer(self.config) as installer:
installer_path = "certbot_postfix.installer"
exe_exists_path = installer_path + ".certbot_util.exe_exists"
path_surgery_path = "certbot_postfix.util.plugins_util.path_surgery"
with mock.patch(path_surgery_path, return_value=False):
with mock.patch(exe_exists_path, return_value=False):
self.assertRaises(errors.NoInstallationError,
installer.prepare)
def test_old_version(self):
with create_installer(self.config, main_cf=_main_cf_with({"mail_version": "0.0.1"}))\
as installer:
self.assertRaises(errors.NotSupportedError, installer.prepare)
def test_lock_error(self):
with create_installer(self.config) as installer:
assert_raises = functools.partial(self.assertRaises,
errors.PluginError,
installer.prepare)
certbot_test_util.lock_and_call(assert_raises, self.tempdir)
@mock.patch('certbot.util.lock_dir_until_exit')
def test_dir_locked(self, lock_dir):
with create_installer(self.config) as installer:
lock_dir.side_effect = errors.LockError
self.assertRaises(errors.PluginError, installer.prepare)
def test_more_info(self):
with create_installer(self.config) as installer:
installer.prepare()
output = installer.more_info()
self.assertTrue("Postfix" in output)
self.assertTrue(self.tempdir in output)
self.assertTrue(DEFAULT_MAIN_CF["mail_version"] in output)
def test_get_all_names(self):
config = {"mydomain": "example.org",
"myhostname": "mail.example.org",
"myorigin": "example.org"}
with create_installer(self.config, main_cf=_main_cf_with(config)) as installer:
installer.prepare()
result = installer.get_all_names()
self.assertEqual(result, set(config.values()))
@certbot_test_util.patch_get_utility()
def test_deploy(self, mock_util):
mock_util().yesno.return_value = True
from certbot_postfix import constants
with create_installer(self.config) as installer:
installer.prepare()
# pylint: disable=protected-access
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
changes = installer.postconf.get_changes()
expected = {} # type: Dict[str, Tuple[str, ...]]
expected.update(constants.TLS_SERVER_VARS)
expected.update(constants.DEFAULT_SERVER_VARS)
expected.update(constants.DEFAULT_CLIENT_VARS)
self.assertEqual(changes["smtpd_tls_key_file"], "key_path")
self.assertEqual(changes["smtpd_tls_cert_file"], "cert_path")
for name, value in six.iteritems(expected):
self.assertEqual(changes[name], value[0])
@certbot_test_util.patch_get_utility()
def test_tls_only(self, mock_util):
mock_util().yesno.return_value = True
with create_installer(self.config) as installer:
installer.prepare()
installer.conf = lambda x: x == "tls_only"
installer.postconf.set = mock.Mock()
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
self.assertEqual(installer.postconf.set.call_count, 4)
@certbot_test_util.patch_get_utility()
def test_server_only(self, mock_util):
mock_util().yesno.return_value = True
with create_installer(self.config) as installer:
installer.prepare()
installer.conf = lambda x: x == "server_only"
installer.postconf.set = mock.Mock()
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
self.assertEqual(installer.postconf.set.call_count, 11)
@certbot_test_util.patch_get_utility()
def test_tls_and_server_only(self, mock_util):
mock_util().yesno.return_value = True
with create_installer(self.config) as installer:
installer.prepare()
installer.conf = lambda x: True
installer.postconf.set = mock.Mock()
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
self.assertEqual(installer.postconf.set.call_count, 3)
@certbot_test_util.patch_get_utility()
def test_deploy_twice(self, mock_util):
# Deploying twice on the same installer shouldn't do anything!
mock_util().yesno.return_value = True
with create_installer(self.config) as installer:
installer.prepare()
from certbot_postfix.postconf import ConfigMain
with mock.patch.object(ConfigMain, "set", wraps=installer.postconf.set) as fake_set:
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
self.assertEqual(fake_set.call_count, 15)
fake_set.reset_mock()
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
fake_set.assert_not_called()
@certbot_test_util.patch_get_utility()
def test_deploy_already_secure(self, mock_util):
# Should not overwrite "more-secure" parameters
mock_util().yesno.return_value = True
more_secure = {
"smtpd_tls_security_level": "encrypt",
"smtpd_tls_protocols": "!SSLv3, !SSLv2, !TLSv1",
"smtpd_tls_eecdh_grade": "strong"
}
with create_installer(self.config,\
main_cf=_main_cf_with(more_secure)) as installer:
installer.prepare()
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
for param in more_secure.keys():
self.assertFalse(param in installer.postconf.get_changes())
def test_enhance(self):
with create_installer(self.config) as installer:
installer.prepare()
self.assertRaises(errors.PluginError,
installer.enhance,
"example.org", "redirect")
def test_supported_enhancements(self):
with create_installer(self.config) as installer:
installer.prepare()
self.assertEqual(installer.supported_enhancements(), [])
@contextmanager
def create_installer(config, main_cf=DEFAULT_MAIN_CF):
# pylint: disable=dangerous-default-value
"""Creates a Postfix installer with calls to `postconf` and `postfix` mocked out.
In particular, creates a ConfigMain object that does regular things, but seeds it
with values from `main_cf` and `master_cf` dicts.
"""
from certbot_postfix.postconf import ConfigMain
from certbot_postfix import installer
def _mock_init_postconf(postconf, executable, ignore_master_overrides=False, config_dir=None):
# pylint: disable=protected-access,unused-argument
postconf._ignore_master_overrides = ignore_master_overrides
postconf._db = main_cf
postconf._master_db = {}
postconf._updated = {}
# override get_default to get from main
postconf.get_default = lambda name: main_cf[name]
with mock.patch.object(ConfigMain, "__init__", _mock_init_postconf):
exe_exists_path = "certbot_postfix.installer.certbot_util.exe_exists"
with mock.patch(exe_exists_path, return_value=True):
with mock.patch("certbot_postfix.installer.util.PostfixUtil",
return_value=mock.Mock()):
yield installer.Installer(config, "postfix")
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -1,107 +0,0 @@
"""Tests for certbot_postfix.postconf."""
import mock
import unittest
from certbot import errors
class PostConfTest(unittest.TestCase):
"""Tests for certbot_postfix.util.PostConf."""
def setUp(self):
from certbot_postfix.postconf import ConfigMain
super(PostConfTest, self).setUp()
with mock.patch('certbot_postfix.util.PostfixUtilBase._get_output') as mock_call:
with mock.patch('certbot_postfix.postconf.ConfigMain._get_output_master') as \
mock_master_call:
with mock.patch('certbot_postfix.postconf.util.verify_exe_exists') as verify_exe:
verify_exe.return_value = True
mock_call.return_value = ('default_parameter = value\n'
'extra_param =\n'
'overridden_by_master = default\n')
mock_master_call.return_value = (
'service/type/overridden_by_master = master_value\n'
'service2/type/overridden_by_master = master_value2\n'
)
self.config = ConfigMain('postconf', False)
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
@mock.patch('certbot_postfix.postconf.util.verify_exe_exists')
def test_get_output_master(self, mock_verify_exe, mock_get_output):
from certbot_postfix.postconf import ConfigMain
mock_verify_exe.return_value = True
ConfigMain('postconf', lambda x, y, z: None)
mock_get_output.assert_called_with('-P')
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
def test_read_default(self, mock_get_output):
mock_get_output.return_value = 'param = default_value'
self.assertEqual(self.config.get_default('param'), 'default_value')
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
def test_set(self, mock_call):
self.config.set('extra_param', 'other_value')
self.assertEqual(self.config.get('extra_param'), 'other_value')
self.config.flush()
mock_call.assert_called_with(['-e', 'extra_param=other_value'])
def test_set_bad_param_name(self):
self.assertRaises(KeyError, self.config.set, 'nonexistent_param', 'some_value')
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
def test_write_revert(self, mock_call):
self.config.set('default_parameter', 'fake_news')
# revert config set
self.config.set('default_parameter', 'value')
self.config.flush()
mock_call.assert_not_called()
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
def test_write_default(self, mock_call):
self.config.set('default_parameter', 'value')
self.config.flush()
mock_call.assert_not_called()
def test_master_overrides(self):
self.assertEqual(self.config.get_master_overrides('overridden_by_master'),
[('service/type', 'master_value'),
('service2/type', 'master_value2')])
def test_set_check_override(self):
self.assertRaises(errors.PluginError, self.config.set,
'overridden_by_master', 'new_value')
def test_ignore_check_override(self):
# pylint: disable=protected-access
self.config._ignore_master_overrides = True
self.config.set('overridden_by_master', 'new_value')
def test_check_acceptable_overrides(self):
self.config.set('overridden_by_master', 'new_value',
('master_value', 'master_value2'))
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
def test_flush(self, mock_out):
self.config.set('default_parameter', 'new_value')
self.config.set('extra_param', 'another_value')
self.config.flush()
arguments = mock_out.call_args_list[-1][0][0]
self.assertEquals('-e', arguments[0])
self.assertTrue('default_parameter=new_value' in arguments)
self.assertTrue('extra_param=another_value' in arguments)
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
def test_flush_updates_object(self, mock_out):
self.config.set('default_parameter', 'new_value')
self.config.flush()
mock_out.reset_mock()
self.config.set('default_parameter', 'new_value')
mock_out.assert_not_called()
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
def test_flush_throws_error_on_fail(self, mock_out):
mock_out.side_effect = [IOError("oh no!")]
self.config.set('default_parameter', 'new_value')
self.assertRaises(errors.PluginError, self.config.flush)
if __name__ == '__main__': # pragma: no cover
unittest.main()

View File

@@ -1,205 +0,0 @@
"""Tests for certbot_postfix.util."""
import subprocess
import unittest
import mock
from certbot import errors
class PostfixUtilBaseTest(unittest.TestCase):
"""Tests for certbot_postfix.util.PostfixUtilBase."""
@classmethod
def _create_object(cls, *args, **kwargs):
from certbot_postfix.util import PostfixUtilBase
return PostfixUtilBase(*args, **kwargs)
@mock.patch('certbot_postfix.util.verify_exe_exists')
def test_no_exe(self, mock_verify):
expected_error = errors.NoInstallationError
mock_verify.side_effect = expected_error
self.assertRaises(expected_error, self._create_object, 'nonexistent')
def test_object_creation(self):
with mock.patch('certbot_postfix.util.verify_exe_exists'):
self._create_object('existent')
@mock.patch('certbot_postfix.util.check_all_output')
def test_call_extends_args(self, mock_output):
# pylint: disable=protected-access
with mock.patch('certbot_postfix.util.verify_exe_exists'):
mock_output.return_value = 'expected'
postfix = self._create_object('executable')
postfix._call(['many', 'extra', 'args'])
mock_output.assert_called_with(['executable', 'many', 'extra', 'args'])
postfix._call()
mock_output.assert_called_with(['executable'])
def test_create_with_config(self):
# pylint: disable=protected-access
with mock.patch('certbot_postfix.util.verify_exe_exists'):
postfix = self._create_object('exec', 'config_dir')
self.assertEqual(postfix._base_command, ['exec', '-c', 'config_dir'])
class PostfixUtilTest(unittest.TestCase):
def setUp(self):
# pylint: disable=protected-access
from certbot_postfix.util import PostfixUtil
with mock.patch('certbot_postfix.util.verify_exe_exists'):
self.postfix = PostfixUtil()
self.postfix._call = mock.Mock()
self.mock_call = self.postfix._call
def test_test(self):
self.postfix.test()
self.mock_call.assert_called_with(['check'])
def test_test_raises_error_when_check_fails(self):
self.mock_call.side_effect = [subprocess.CalledProcessError(1, "")]
self.assertRaises(errors.MisconfigurationError, self.postfix.test)
self.mock_call.assert_called_with(['check'])
def test_restart_while_running(self):
self.mock_call.side_effect = [subprocess.CalledProcessError(1, ""), None]
self.postfix.restart()
self.mock_call.assert_called_with(['start'])
def test_restart_while_not_running(self):
self.postfix.restart()
self.mock_call.assert_called_with(['reload'])
def test_restart_raises_error_when_reload_fails(self):
self.mock_call.side_effect = [None, subprocess.CalledProcessError(1, "")]
self.assertRaises(errors.PluginError, self.postfix.restart)
self.mock_call.assert_called_with(['reload'])
def test_restart_raises_error_when_start_fails(self):
self.mock_call.side_effect = [
subprocess.CalledProcessError(1, ""),
subprocess.CalledProcessError(1, "")]
self.assertRaises(errors.PluginError, self.postfix.restart)
self.mock_call.assert_called_with(['start'])
class CheckAllOutputTest(unittest.TestCase):
"""Tests for certbot_postfix.util.check_all_output."""
@classmethod
def _call(cls, *args, **kwargs):
from certbot_postfix.util import check_all_output
return check_all_output(*args, **kwargs)
@mock.patch('certbot_postfix.util.logger')
@mock.patch('certbot_postfix.util.subprocess.Popen')
def test_command_error(self, mock_popen, mock_logger):
command = 'foo'
retcode = 42
output = 'bar'
err = 'baz'
mock_popen().communicate.return_value = (output, err)
mock_popen().poll.return_value = 42
self.assertRaises(subprocess.CalledProcessError, self._call, command)
log_args = mock_logger.debug.call_args[0]
for value in (command, retcode, output, err,):
self.assertTrue(value in log_args)
@mock.patch('certbot_postfix.util.subprocess.Popen')
def test_success(self, mock_popen):
command = 'foo'
expected = ('bar', '')
mock_popen().communicate.return_value = expected
mock_popen().poll.return_value = 0
self.assertEqual(self._call(command), expected)
def test_stdout_error(self):
self.assertRaises(ValueError, self._call, stdout=None)
def test_stderr_error(self):
self.assertRaises(ValueError, self._call, stderr=None)
def test_universal_newlines_error(self):
self.assertRaises(ValueError, self._call, universal_newlines=False)
class VerifyExeExistsTest(unittest.TestCase):
"""Tests for certbot_postfix.util.verify_exe_exists."""
@classmethod
def _call(cls, *args, **kwargs):
from certbot_postfix.util import verify_exe_exists
return verify_exe_exists(*args, **kwargs)
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
@mock.patch('certbot_postfix.util.plugins_util.path_surgery')
def test_failure(self, mock_exe_exists, mock_path_surgery):
mock_exe_exists.return_value = mock_path_surgery.return_value = False
self.assertRaises(errors.NoInstallationError, self._call, 'foo')
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
def test_simple_success(self, mock_exe_exists):
mock_exe_exists.return_value = True
self._call('foo')
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
@mock.patch('certbot_postfix.util.plugins_util.path_surgery')
def test_successful_surgery(self, mock_exe_exists, mock_path_surgery):
mock_exe_exists.return_value = False
mock_path_surgery.return_value = True
self._call('foo')
class TestUtils(unittest.TestCase):
""" Testing random utility functions in util.py
"""
def test_report_master_overrides(self):
from certbot_postfix.util import report_master_overrides
self.assertRaises(errors.PluginError, report_master_overrides, 'name',
[('service/type', 'value')])
# Shouldn't raise error
report_master_overrides('name', [('service/type', 'value')],
acceptable_overrides=('value',))
def test_no_acceptable_value(self):
from certbot_postfix.util import is_acceptable_value
self.assertFalse(is_acceptable_value('name', 'value', None))
def test_is_acceptable_value(self):
from certbot_postfix.util import is_acceptable_value
self.assertTrue(is_acceptable_value('name', 'value', ('value',)))
self.assertFalse(is_acceptable_value('name', 'bad', ('value',)))
def test_is_acceptable_tuples(self):
from certbot_postfix.util import is_acceptable_value
self.assertTrue(is_acceptable_value('name', 'value', ('value', 'value1')))
self.assertFalse(is_acceptable_value('name', 'bad', ('value', 'value1')))
def test_is_acceptable_protocols(self):
from certbot_postfix.util import is_acceptable_value
# SSLv2 and SSLv3 are both not supported, unambiguously
self.assertFalse(is_acceptable_value('tls_mandatory_protocols_lol',
'SSLv2, SSLv3', None))
self.assertFalse(is_acceptable_value('tls_protocols_lol',
'SSLv2, SSLv3', None))
self.assertFalse(is_acceptable_value('tls_protocols_lol',
'!SSLv2, !TLSv1', None))
self.assertFalse(is_acceptable_value('tls_protocols_lol',
'!SSLv2, SSLv3, !SSLv3, ', None))
self.assertTrue(is_acceptable_value('tls_protocols_lol',
'!SSLv2, !SSLv3', None))
self.assertTrue(is_acceptable_value('tls_protocols_lol',
'!SSLv3, !TLSv1, !SSLv2', None))
# TLSv1.2 is supported unambiguously
self.assertFalse(is_acceptable_value('tls_protocols_lol',
'TLSv1, TLSv1.1,', None))
self.assertFalse(is_acceptable_value('tls_protocols_lol',
'TLSv1.2, !TLSv1.2,', None))
self.assertTrue(is_acceptable_value('tls_protocols_lol',
'TLSv1.2, ', None))
self.assertTrue(is_acceptable_value('tls_protocols_lol',
'TLSv1, TLSv1.1, TLSv1.2', None))
if __name__ == '__main__': # pragma: no cover
unittest.main()

View File

@@ -1,292 +0,0 @@
"""Utility functions for use in the Postfix installer."""
import logging
import re
import subprocess
from certbot import errors
from certbot import util as certbot_util
from certbot.plugins import util as plugins_util
from certbot_postfix import constants
logger = logging.getLogger(__name__)
COMMAND = "postfix"
class PostfixUtilBase(object):
"""A base class for wrapping Postfix command line utilities."""
def __init__(self, executable, config_dir=None):
"""Sets up the Postfix utility class.
:param str executable: name or path of the Postfix utility
:param str config_dir: path to an alternative Postfix config
:raises .NoInstallationError: when the executable isn't found
"""
self.executable = executable
verify_exe_exists(executable)
self._set_base_command(config_dir)
self.config_dir = None
def _set_base_command(self, config_dir):
self._base_command = [self.executable]
if config_dir is not None:
self._base_command.extend(('-c', config_dir,))
def _call(self, extra_args=None):
"""Runs the Postfix utility and returns the result.
:param list extra_args: additional arguments for the command
:returns: data written to stdout and stderr
:rtype: `tuple` of `str`
:raises subprocess.CalledProcessError: if the command fails
"""
args = list(self._base_command)
if extra_args is not None:
args.extend(extra_args)
return check_all_output(args)
def _get_output(self, extra_args=None):
"""Runs the Postfix utility and returns only stdout output.
This function relies on self._call for running the utility.
:param list extra_args: additional arguments for the command
:returns: data written to stdout
:rtype: str
:raises subprocess.CalledProcessError: if the command fails
"""
return self._call(extra_args)[0]
class PostfixUtil(PostfixUtilBase):
"""Wrapper around Postfix CLI tool.
"""
def __init__(self, config_dir=None):
super(PostfixUtil, self).__init__(COMMAND, config_dir)
def test(self):
"""Make sure the configuration is valid.
:raises .MisconfigurationError: if the config is invalid
"""
try:
self._call(["check"])
except subprocess.CalledProcessError as e:
logger.debug("Could not check postfix configuration:\n%s", e)
raise errors.MisconfigurationError(
"Postfix failed internal configuration check.")
def restart(self):
"""Restart or refresh the server content.
:raises .PluginError: when server cannot be restarted
"""
logger.info("Reloading Postfix configuration...")
if self._is_running():
self._reload()
else:
self._start()
def _is_running(self):
"""Is Postfix currently running?
Uses the 'postfix status' command to determine if Postfix is
currently running using the specified configuration files.
:returns: True if Postfix is running, otherwise, False
:rtype: bool
"""
try:
self._call(["status"])
except subprocess.CalledProcessError:
return False
return True
def _start(self):
"""Instructions Postfix to start running.
:raises .PluginError: when Postfix cannot start
"""
try:
self._call(["start"])
except subprocess.CalledProcessError:
raise errors.PluginError("Postfix failed to start")
def _reload(self):
"""Instructs Postfix to reload its configuration.
If Postfix isn't currently running, this method will fail.
:raises .PluginError: when Postfix cannot reload
"""
try:
self._call(["reload"])
except subprocess.CalledProcessError:
raise errors.PluginError(
"Postfix failed to reload its configuration")
def check_all_output(*args, **kwargs):
"""A version of subprocess.check_output that also captures stderr.
This is the same as :func:`subprocess.check_output` except output
written to stderr is also captured and returned to the caller. The
return value is a tuple of two strings (rather than byte strings).
To accomplish this, the caller cannot set the stdout, stderr, or
universal_newlines parameters to :class:`subprocess.Popen`.
Additionally, if the command exits with a nonzero status, output is
not included in the raised :class:`subprocess.CalledProcessError`
because Python 2.6 does not support this. Instead, the failure
including the output is logged.
:param tuple args: positional arguments for Popen
:param dict kwargs: keyword arguments for Popen
:returns: data written to stdout and stderr
:rtype: `tuple` of `str`
:raises ValueError: if arguments are invalid
:raises subprocess.CalledProcessError: if the command fails
"""
for keyword in ('stdout', 'stderr', 'universal_newlines',):
if keyword in kwargs:
raise ValueError(
keyword + ' argument not allowed, it will be overridden.')
kwargs['stdout'] = subprocess.PIPE
kwargs['stderr'] = subprocess.PIPE
kwargs['universal_newlines'] = True
process = subprocess.Popen(*args, **kwargs)
output, err = process.communicate()
retcode = process.poll()
if retcode:
cmd = kwargs.get('args')
if cmd is None:
cmd = args[0]
logger.debug(
"'%s' exited with %d. stdout output was:\n%s\nstderr output was:\n%s",
cmd, retcode, output, err)
raise subprocess.CalledProcessError(retcode, cmd)
return (output, err)
def verify_exe_exists(exe, message=None):
"""Ensures an executable with the given name is available.
If an executable isn't found for the given path or name, extra
directories are added to the user's PATH to help find system
utilities that may not be available in the default cron PATH.
:param str exe: executable path or name
:param str message: Error message to print.
:raises .NoInstallationError: when the executable isn't found
"""
if message is None:
message = "Cannot find executable '{0}'.".format(exe)
if not (certbot_util.exe_exists(exe) or plugins_util.path_surgery(exe)):
raise errors.NoInstallationError(message)
def report_master_overrides(name, overrides, acceptable_overrides=None):
"""If the value for a parameter `name` is overridden by other services,
report a warning to notify the user. If `parameter` is a TLS version parameter
(i.e., `parameter` contains 'tls_protocols' or 'tls_mandatory_protocols'), then
`acceptable_overrides` isn't used each value in overrides is inspected for secure TLS
versions.
:param str name: The name of the parameter that is being overridden.
:param list overrides: The values that other services are setting for `name`.
Each override is a tuple: (service name, value)
:param tuple acceptable_overrides: Override values that are acceptable. For instance, if
another service is overriding our parameter with a more secure option, we don't have
to warn. If this is set to None, errors are raised for *any* overrides of `name`!
"""
error_string = ""
for override in overrides:
service, value = override
# If this override is acceptable:
if acceptable_overrides is not None and \
is_acceptable_value(name, value, acceptable_overrides):
continue
error_string += " {0}: {1}\n".format(service, value)
if error_string:
raise errors.PluginError("{0} is overridden with less secure options by the "
"following services in master.cf:\n".format(name) + error_string)
def is_acceptable_value(parameter, value, acceptable=None):
""" Returns whether the `value` for this `parameter` is acceptable,
given a tuple of `acceptable` values. If `parameter` is a TLS version parameter
(i.e., `parameter` contains 'tls_protocols' or 'tls_mandatory_protocols'), then
`acceptable` isn't used and `value` is inspected for secure TLS versions.
:param str parameter: The name of the parameter being set.
:param str value: Proposed new value for parameter.
:param tuple acceptable: List of acceptable values for parameter.
"""
# Check if param value is a comma-separated list of protocols.
# Otherwise, just check whether the value is in the acceptable list.
if 'tls_protocols' in parameter or 'tls_mandatory_protocols' in parameter:
return _has_acceptable_tls_versions(value)
if acceptable is not None:
return value in acceptable
return False
def _has_acceptable_tls_versions(parameter_string):
"""
Checks to see if the list of TLS protocols is acceptable.
This requires that TLSv1.2 is supported, and neither SSLv2 nor SSLv3 are supported.
Should be a string of protocol names delimited by commas, spaces, or colons.
Postfix's documents suggest listing protocols to exclude, like "!SSLv2, !SSLv3".
Listing the protocols to include, like "TLSv1, TLSv1.1, TLSv1.2" is okay as well,
though not recommended
When these two modes are interspersed, the presence of a single non-negated protocol name
(i.e. "TLSv1" rather than "!TLSv1") automatically excludes all other unnamed protocols.
In addition, the presence of both a protocol name inclusion and exclusion isn't explicitly
documented, so this method should return False if it encounters contradicting statements
about TLSv1.2, SSLv2, or SSLv3. (for instance, "SSLv3, !SSLv3").
"""
if not parameter_string:
return False
bad_versions = list(constants.TLS_VERSIONS)
for version in constants.ACCEPTABLE_TLS_VERSIONS:
del bad_versions[bad_versions.index(version)]
supported_version_list = re.split("[, :]+", parameter_string)
# The presence of any non-"!" protocol listing excludes the others by default.
inclusion_list = False
for version in supported_version_list:
if not version:
continue
if version in bad_versions: # short-circuit if we recognize any bad version
return False
if version[0] != "!":
inclusion_list = True
if inclusion_list: # For any inclusion list, we still require TLS 1.2.
if "TLSv1.2" not in supported_version_list or "!TLSv1.2" in supported_version_list:
return False
else:
for bad_version in bad_versions:
if "!" + bad_version not in supported_version_list:
return False
return True

View File

@@ -1 +0,0 @@
/_build/

View File

@@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = certbot-postfix
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

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

View File

@@ -1,5 +0,0 @@
:mod:`certbot_postfix.installer`
--------------------------------------
.. automodule:: certbot_postfix.installer
:members:

View File

@@ -1,5 +0,0 @@
:mod:`certbot_postfix.postconf`
-------------------------------
.. automodule:: certbot_postfix.postconf
:members:

View File

@@ -1,190 +0,0 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = u'certbot-postfix'
copyright = u'2018, Certbot Project'
author = u'Certbot Project'
# The short X.Y version
version = u'0'
# The full version, including alpha/beta/rc tags
release = u'0'
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
]
autodoc_member_order = 'bysource'
autodoc_default_flags = ['show-inheritance', 'private-members']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = u'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path .
exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
default_role = 'py:obj'
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs
# on_rtd is whether we are on readthedocs.org
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd: # only import and set the theme if we're building docs locally
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# otherwise, readthedocs.org uses their theme by default, so no need to specify it
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'certbot-postfixdoc'
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'certbot-postfix.tex', u'certbot-postfix Documentation',
u'Certbot Project', 'manual'),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'certbot-postfix', u'certbot-postfix Documentation',
[author], 1)
]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'certbot-postfix', u'certbot-postfix Documentation',
author, 'certbot-postfix', 'One line description of project.',
'Miscellaneous'),
]
# -- Extension configuration -------------------------------------------------
# -- Options for intersphinx extension ---------------------------------------
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'python': ('https://docs.python.org/', None),
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
'certbot': ('https://certbot.eff.org/docs/', None),
}
# -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True

View File

@@ -1,28 +0,0 @@
.. certbot-postfix documentation master file, created by
sphinx-quickstart on Wed May 2 16:01:06 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to certbot-postfix's documentation!
===========================================
.. toctree::
:maxdepth: 2
:caption: Contents:
.. automodule:: certbot_postfix
:members:
.. toctree::
:maxdepth: 1
api
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@@ -1,36 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=certbot-postfix
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

View File

@@ -1,2 +0,0 @@
acme[dev]==0.25.0
certbot[dev]==0.23.0

View File

@@ -1,2 +0,0 @@
[bdist_wheel]
universal = 1

View File

@@ -1,63 +0,0 @@
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
install_requires = [
'acme>=0.25.0',
'certbot>=0.23.0',
'setuptools',
'six',
'zope.component',
'zope.interface',
]
docs_extras = [
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
'sphinx_rtd_theme',
]
setup(
name='certbot-postfix',
version=version,
description="Postfix plugin for Certbot",
url='https://github.com/certbot/certbot',
author="Certbot Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Plugins',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Communications :: Email :: Mail Transport Agents',
'Topic :: Security',
'Topic :: System :: Installation/Setup',
'Topic :: System :: Networking',
'Topic :: System :: Systems Administration',
'Topic :: Utilities',
],
packages=find_packages(),
include_package_data=True,
install_requires=install_requires,
extras_require={
'docs': docs_extras,
},
entry_points={
'certbot.plugins': [
'postfix = certbot_postfix:Installer',
],
},
test_suite='certbot_postfix',
)

View File

@@ -1,4 +1,4 @@
"""Certbot client."""
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
__version__ = '0.26.0.dev0'
__version__ = '0.25.0.dev0'

View File

@@ -16,7 +16,6 @@ import zope.component
from acme import fields as acme_fields
from acme import messages
from certbot import constants
from certbot import errors
from certbot import interfaces
from certbot import util
@@ -143,11 +142,7 @@ class AccountFileStorage(interfaces.AccountStorage):
self.config.strict_permissions)
def _account_dir_path(self, account_id):
return self._account_dir_path_for_server_path(account_id, self.config.server_path)
def _account_dir_path_for_server_path(self, account_id, server_path):
accounts_dir = self.config.accounts_dir_for_server_path(server_path)
return os.path.join(accounts_dir, account_id)
return os.path.join(self.config.accounts_dir, account_id)
@classmethod
def _regr_path(cls, account_dir_path):
@@ -161,57 +156,25 @@ class AccountFileStorage(interfaces.AccountStorage):
def _metadata_path(cls, account_dir_path):
return os.path.join(account_dir_path, "meta.json")
def _find_all_for_server_path(self, server_path):
accounts_dir = self.config.accounts_dir_for_server_path(server_path)
def find_all(self):
try:
candidates = os.listdir(accounts_dir)
candidates = os.listdir(self.config.accounts_dir)
except OSError:
return []
accounts = []
for account_id in candidates:
try:
accounts.append(self._load_for_server_path(account_id, server_path))
accounts.append(self.load(account_id))
except errors.AccountStorageError:
logger.debug("Account loading problem", exc_info=True)
if not accounts and server_path in constants.LE_REUSE_SERVERS:
# find all for the next link down
prev_server_path = constants.LE_REUSE_SERVERS[server_path]
prev_accounts = self._find_all_for_server_path(prev_server_path)
# if we found something, link to that
if prev_accounts:
try:
self._symlink_to_accounts_dir(prev_server_path, server_path)
except OSError:
return []
accounts = prev_accounts
return accounts
def find_all(self):
return self._find_all_for_server_path(self.config.server_path)
def _symlink_to_accounts_dir(self, prev_server_path, server_path):
accounts_dir = self.config.accounts_dir_for_server_path(server_path)
if os.path.islink(accounts_dir):
os.unlink(accounts_dir)
else:
os.rmdir(accounts_dir)
prev_account_dir = self.config.accounts_dir_for_server_path(prev_server_path)
os.symlink(prev_account_dir, accounts_dir)
def _load_for_server_path(self, account_id, server_path):
account_dir_path = self._account_dir_path_for_server_path(account_id, server_path)
if not os.path.isdir(account_dir_path): # isdir is also true for symlinks
if server_path in constants.LE_REUSE_SERVERS:
prev_server_path = constants.LE_REUSE_SERVERS[server_path]
prev_loaded_account = self._load_for_server_path(account_id, prev_server_path)
# we didn't error so we found something, so create a symlink to that
self._symlink_to_accounts_dir(prev_server_path, server_path)
return prev_loaded_account
else:
raise errors.AccountNotFound(
"Account at %s does not exist" % account_dir_path)
def load(self, account_id):
account_dir_path = self._account_dir_path(account_id)
if not os.path.isdir(account_dir_path):
raise errors.AccountNotFound(
"Account at %s does not exist" % account_dir_path)
try:
with open(self._regr_path(account_dir_path)) as regr_file:
@@ -230,9 +193,6 @@ class AccountFileStorage(interfaces.AccountStorage):
account_id, acc.id))
return acc
def load(self, account_id):
return self._load_for_server_path(account_id, self.config.server_path)
def save(self, account, acme):
self._save(account, acme, regr_only=False)

View File

@@ -32,7 +32,6 @@ from certbot import util
from certbot.display import util as display_util
from certbot.plugins import disco as plugins_disco
import certbot.plugins.enhancements as enhancements
import certbot.plugins.selection as plugin_selection
logger = logging.getLogger(__name__)
@@ -628,10 +627,6 @@ class HelpfulArgumentParser(object):
raise errors.Error("Using --allow-subset-of-names with a"
" wildcard domain is not supported.")
if parsed_args.hsts and parsed_args.auto_hsts:
raise errors.Error(
"Parameters --hsts and --auto-hsts cannot be used simultaneously.")
possible_deprecation_warning(parsed_args)
return parsed_args
@@ -1022,12 +1017,6 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
"certificate already exists for the requested certificate name "
"but does not match the requested domains, renew it now, "
"regardless of whether it is near expiry.")
helpful.add(
"automation", "--reuse-key", dest="reuse_key",
action="store_true", default=flag_default("reuse_key"),
help="When renewing, use the same private key as the existing "
"certificate.")
helpful.add(
["automation", "renew", "certonly"],
"--allow-subset-of-names", action="store_true",
@@ -1082,7 +1071,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
help="Show tracebacks in case of errors, and allow certbot-auto "
"execution on experimental platforms")
helpful.add(
[None, "certonly", "run"], "--debug-challenges", action="store_true",
[None, "certonly", "renew", "run"], "--debug-challenges", action="store_true",
default=flag_default("debug_challenges"),
help="After setting up challenges, wait for user input before "
"submitting to CA")
@@ -1225,17 +1214,10 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
" when the user executes \"certbot renew\", regardless of if the certificate"
" is renewed. This setting does not apply to important TLS configuration"
" updates.")
helpful.add(
"renew", "--no-autorenew", action="store_false",
default=flag_default("autorenew"), dest="autorenew",
help="Disable auto renewal of certificates.")
helpful.add_deprecated_argument("--agree-dev-preview", 0)
helpful.add_deprecated_argument("--dialog", 0)
# Populate the command line parameters for new style enhancements
enhancements.populate_cli(helpful.add)
_create_subparsers(helpful)
_paths_parser(helpful)
# _plugins_parsing should be the last thing to act upon the main

View File

@@ -4,7 +4,6 @@ import logging
import os
import platform
from cryptography.hazmat.backends import default_backend
# https://github.com/python/typeshed/blob/master/third_party/
# 2/cryptography/hazmat/primitives/asymmetric/rsa.pyi
@@ -17,7 +16,6 @@ from acme import client as acme_client
from acme import crypto_util as acme_crypto_util
from acme import errors as acme_errors
from acme import messages
from acme.magic_typing import Optional # pylint: disable=unused-import,no-name-in-module
import certbot
@@ -65,17 +63,9 @@ def determine_user_agent(config):
if config.user_agent is None:
ua = ("CertbotACMEClient/{0} ({1}; {2}{8}) Authenticator/{3} Installer/{4} "
"({5}; flags: {6}) Py/{7}")
if os.environ.get("CERTBOT_DOCS") == "1":
cli_command = "certbot(-auto)"
os_info = "OS_NAME OS_VERSION"
python_version = "major.minor.patchlevel"
else:
cli_command = cli.cli_command
os_info = util.get_os_info_ua()
python_version = platform.python_version()
ua = ua.format(certbot.__version__, cli_command, os_info,
ua = ua.format(certbot.__version__, cli.cli_command, util.get_os_info_ua(),
config.authenticator, config.installer, config.verb,
ua_flags(config), python_version,
ua_flags(config), platform.python_version(),
"; " + config.user_agent_comment if config.user_agent_comment else "")
else:
ua = config.user_agent
@@ -283,7 +273,7 @@ class Client(object):
cert, chain = crypto_util.cert_and_chain_from_fullchain(orderr.fullchain_pem)
return cert.encode(), chain.encode()
def obtain_certificate(self, domains, old_keypath=None):
def obtain_certificate(self, domains):
"""Obtains a certificate from the ACME server.
`.register` must be called before `.obtain_certificate`
@@ -296,39 +286,16 @@ class Client(object):
:rtype: tuple
"""
# We need to determine the key path, key PEM data, CSR path,
# and CSR PEM data. For a dry run, the paths are None because
# they aren't permanently saved to disk. For a lineage with
# --reuse-key, the key path and PEM data are derived from an
# existing file.
if old_keypath is not None:
# We've been asked to reuse a specific existing private key.
# Therefore, we'll read it now and not generate a new one in
# either case below.
#
# We read in bytes here because the type of `key.pem`
# created below is also bytes.
with open(old_keypath, "rb") as f:
keypath = old_keypath
keypem = f.read()
key = util.Key(file=keypath, pem=keypem) # type: Optional[util.Key]
logger.info("Reusing existing private key from %s.", old_keypath)
else:
# The key is set to None here but will be created below.
key = None
# Create CSR from names
if self.config.dry_run:
key = key or util.Key(file=None,
pem=crypto_util.make_key(self.config.rsa_key_size))
key = util.Key(file=None,
pem=crypto_util.make_key(self.config.rsa_key_size))
csr = util.CSR(file=None, form="pem",
data=acme_crypto_util.make_csr(
key.pem, domains, self.config.must_staple))
else:
key = key or crypto_util.init_save_key(self.config.rsa_key_size,
self.config.key_dir)
key = crypto_util.init_save_key(
self.config.rsa_key_size, self.config.key_dir)
csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
orderr = self._get_order_and_authorizations(csr.data, self.config.allow_subset_of_names)

View File

@@ -65,12 +65,8 @@ class NamespaceConfig(object):
@property
def accounts_dir(self): # pylint: disable=missing-docstring
return self.accounts_dir_for_server_path(self.server_path)
def accounts_dir_for_server_path(self, server_path):
"""Path to accounts directory based on server_path"""
return os.path.join(
self.namespace.config_dir, constants.ACCOUNTS_DIR, server_path)
self.namespace.config_dir, constants.ACCOUNTS_DIR, self.server_path)
@property
def backup_dir(self): # pylint: disable=missing-docstring

View File

@@ -37,7 +37,6 @@ CLI_DEFAULTS = dict(
expand=False,
renew_by_default=False,
renew_with_new_domains=False,
autorenew=True,
allow_subset_of_names=False,
tos=False,
account=None,
@@ -58,7 +57,6 @@ CLI_DEFAULTS = dict(
rsa_key_size=2048,
must_staple=False,
redirect=None,
auto_hsts=False,
hsts=None,
uir=None,
staple=None,
@@ -66,7 +64,6 @@ CLI_DEFAULTS = dict(
pref_challs=[],
validate_hooks=True,
directory_hooks=True,
reuse_key=False,
disable_renew_updates=False,
# Subparsers
@@ -160,13 +157,6 @@ CONFIG_DIRS_MODE = 0o755
ACCOUNTS_DIR = "accounts"
"""Directory where all accounts are saved."""
LE_REUSE_SERVERS = {
'acme-v02.api.letsencrypt.org/directory': 'acme-v01.api.letsencrypt.org/directory',
'acme-staging-v02.api.letsencrypt.org/directory':
'acme-staging.api.letsencrypt.org/directory'
}
"""Servers that can reuse accounts from other servers."""
BACKUP_DIR = "backups"
"""Directory (relative to `IConfig.work_dir`) where backups are kept."""

View File

@@ -7,7 +7,7 @@
import hashlib
import logging
import os
import warnings
import pyrfc3339
import six
@@ -237,23 +237,21 @@ def verify_renewable_cert_sig(renewable_cert):
with open(renewable_cert.cert, 'rb') as cert_file: # type: IO[bytes]
cert = x509.load_pem_x509_certificate(cert_file.read(), default_backend())
pk = chain.public_key()
with warnings.catch_warnings():
warnings.simplefilter("ignore")
if isinstance(pk, RSAPublicKey):
# https://github.com/python/typeshed/blob/master/third_party/2/cryptography/hazmat/primitives/asymmetric/rsa.pyi
verifier = pk.verifier( # type: ignore
cert.signature, PKCS1v15(), cert.signature_hash_algorithm
)
verifier.update(cert.tbs_certificate_bytes)
verifier.verify()
elif isinstance(pk, EllipticCurvePublicKey):
verifier = pk.verifier(
cert.signature, ECDSA(cert.signature_hash_algorithm)
)
verifier.update(cert.tbs_certificate_bytes)
verifier.verify()
else:
raise errors.Error("Unsupported public key type")
if isinstance(pk, RSAPublicKey):
# https://github.com/python/typeshed/blob/master/third_party/2/cryptography/hazmat/primitives/asymmetric/rsa.pyi
verifier = pk.verifier( # type: ignore
cert.signature, PKCS1v15(), cert.signature_hash_algorithm
)
verifier.update(cert.tbs_certificate_bytes)
verifier.verify()
elif isinstance(pk, EllipticCurvePublicKey):
verifier = pk.verifier(
cert.signature, ECDSA(cert.signature_hash_algorithm)
)
verifier.update(cert.tbs_certificate_bytes)
verifier.verify()
else:
raise errors.Error("Unsupported public key type")
except (IOError, ValueError, InvalidSignature) as e:
error_str = "verifying the signature of the cert located at {0} has failed. \
Details: {1}".format(renewable_cert.cert, e)

View File

@@ -29,10 +29,6 @@ HELP = "help"
ESC = "esc"
"""Display exit code when the user hits Escape (UNUSED)"""
# Display constants
SIDE_FRAME = ("- " * 39) + "-"
"""Display boundary (alternates spaces, so when copy-pasted, markdown doesn't interpret
it as a heading)"""
def _wrap_lines(msg):
"""Format lines nicely to 80 chars.
@@ -115,11 +111,12 @@ class FileDisplay(object):
because it won't cause any workflow regressions
"""
side_frame = "-" * 79
if wrap:
message = _wrap_lines(message)
self.outfile.write(
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
line=os.linesep, frame=SIDE_FRAME, msg=message))
line=os.linesep, frame=side_frame, msg=message))
self.outfile.flush()
if pause:
if self._can_interact(force_interactive):
@@ -211,10 +208,12 @@ class FileDisplay(object):
if self._return_default(message, default, cli_flag, force_interactive):
return default
side_frame = ("-" * 79) + os.linesep
message = _wrap_lines(message)
self.outfile.write("{0}{frame}{msg}{0}{frame}".format(
os.linesep, frame=SIDE_FRAME + os.linesep, msg=message))
os.linesep, frame=side_frame, msg=message))
self.outfile.flush()
while True:
@@ -387,7 +386,8 @@ class FileDisplay(object):
# Write out the message to the user
self.outfile.write(
"{new}{msg}{new}".format(new=os.linesep, msg=message))
self.outfile.write(SIDE_FRAME + os.linesep)
side_frame = ("-" * 79) + os.linesep
self.outfile.write(side_frame)
# Write out the menu choices
for i, desc in enumerate(choices, 1):
@@ -397,7 +397,7 @@ class FileDisplay(object):
# Keep this outside of the textwrap
self.outfile.write(os.linesep)
self.outfile.write(SIDE_FRAME + os.linesep)
self.outfile.write(side_frame)
self.outfile.flush()
def _get_valid_int_ans(self, max_):
@@ -482,11 +482,12 @@ class NoninteractiveDisplay(object):
:param bool wrap: Whether or not the application should wrap text
"""
side_frame = "-" * 79
if wrap:
message = _wrap_lines(message)
self.outfile.write(
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
line=os.linesep, frame=SIDE_FRAME, msg=message))
line=os.linesep, frame=side_frame, msg=message))
self.outfile.flush()
def menu(self, message, choices, ok_label=None, cancel_label=None,

View File

@@ -604,10 +604,10 @@ class IReporter(zope.interface.Interface):
# When "certbot renew" is run, Certbot will iterate over each lineage and check
# if the selected installer for that lineage is a subclass of each updater
# class. If it is and the update of that type is configured to be run for that
# lineage, the relevant update function will be called for it. These functions
# are never called for other subcommands, so if an installer wants to perform
# an update during the run or install subcommand, it should do so when
# :func:`IInstaller.deploy_cert` is called.
# lineage, the relevant update function will be called for each domain in the
# lineage. These functions are never called for other subcommands, so if an
# installer wants to perform an update during the run or install subcommand, it
# should do so when :func:`IInstaller.deploy_cert` is called.
@six.add_metaclass(abc.ABCMeta)
class GenericUpdater(object):
@@ -623,7 +623,7 @@ class GenericUpdater(object):
"""
@abc.abstractmethod
def generic_updates(self, lineage, *args, **kwargs):
def generic_updates(self, domain, *args, **kwargs):
"""Perform any update types defined by the installer.
If an installer is a subclass of the class containing this method, this
@@ -631,10 +631,9 @@ class GenericUpdater(object):
update defined by the installer should be run conditionally, the
installer needs to handle checking the conditions itself.
This method is called once for each lineage.
This method is called once for each domain.
:param lineage: Certificate lineage object
:type lineage: storage.RenewableCert
:param str domain: domain to handle the updates for
"""
@@ -662,7 +661,8 @@ class RenewDeployer(object):
This method is called once for each lineage renewed
:param lineage: Certificate lineage object
:param lineage: Certificate lineage object that is set if certificate
was renewed on this run.
:type lineage: storage.RenewableCert
"""

View File

@@ -36,7 +36,7 @@ from certbot import util
from certbot.display import util as display_util, ops as display_ops
from certbot.plugins import disco as plugins_disco
from certbot.plugins import selection as plug_sel
from certbot.plugins import enhancements
USER_CANCELLED = ("User chose to cancel the operation and may "
"reinvoke the client.")
@@ -473,7 +473,8 @@ def _report_new_cert(config, cert_path, fullchain_path, key_path=None):
def _determine_account(config):
"""Determine which account to use.
If ``config.account`` is ``None``, it will be updated based on the
In order to make the renewer (configuration de/serialization) happy,
if ``config.account`` is ``None``, it will be updated based on the
user input. Same for ``config.email``.
:param config: Configuration object
@@ -750,8 +751,8 @@ def _install_cert(config, le_client, domains, lineage=None):
:param le_client: Client object
:type le_client: client.Client
:param domains: List of domains
:type domains: `list` of `str`
:param plugins: List of domains
:type plugins: `list` of `str`
:param lineage: Certificate lineage object. Defaults to `None`
:type lineage: storage.RenewableCert
@@ -789,26 +790,11 @@ def install(config, plugins):
except errors.PluginSelectionError as e:
return str(e)
custom_cert = (config.key_path and config.cert_path)
if not config.certname and not custom_cert:
certname_question = "Which certificate would you like to install?"
config.certname = cert_manager.get_certnames(
config, "install", allow_multiple=False,
custom_prompt=certname_question)[0]
if not enhancements.are_supported(config, installer):
raise errors.NotSupportedError("One ore more of the requested enhancements "
"are not supported by the selected installer")
# If cert-path is defined, populate missing (ie. not overridden) values.
# Unfortunately this can't be done in argument parser, as certificate
# manager needs the access to renewal directory paths
if config.certname:
config = _populate_from_certname(config)
elif enhancements.are_requested(config):
# Preflight config check
raise errors.ConfigurationError("One or more of the requested enhancements "
"require --cert-name to be provided")
if config.key_path and config.cert_path:
_check_certificate_and_key(config)
domains, _ = _find_domains_or_certname(config, installer)
@@ -819,11 +805,6 @@ def install(config, plugins):
"If your certificate is managed by Certbot, please use --cert-name "
"to define which certificate you would like to install.")
if enhancements.are_requested(config):
# In the case where we don't have certname, we have errored out already
lineage = cert_manager.lineage_for_certname(config, config.certname)
enhancements.enable(lineage, domains, installer, config)
def _populate_from_certname(config):
"""Helper function for install to populate missing config values from lineage
defined by --cert-name."""
@@ -901,8 +882,7 @@ def enhance(config, plugins):
"""
supported_enhancements = ["hsts", "redirect", "uir", "staple"]
# Check that at least one enhancement was requested on command line
oldstyle_enh = any([getattr(config, enh) for enh in supported_enhancements])
if not enhancements.are_requested(config) and not oldstyle_enh:
if not any([getattr(config, enh) for enh in supported_enhancements]):
msg = ("Please specify one or more enhancement types to configure. To list "
"the available enhancement types, run:\n\n%s --help enhance\n")
logger.warning(msg, sys.argv[0])
@@ -913,10 +893,6 @@ def enhance(config, plugins):
except errors.PluginSelectionError as e:
return str(e)
if not enhancements.are_supported(config, installer):
raise errors.NotSupportedError("One ore more of the requested enhancements "
"are not supported by the selected installer")
certname_question = ("Which certificate would you like to use to enhance "
"your configuration?")
config.certname = cert_manager.get_certnames(
@@ -932,15 +908,11 @@ def enhance(config, plugins):
if not domains:
raise errors.Error("User cancelled the domain selection. No domains "
"defined, exiting.")
lineage = cert_manager.lineage_for_certname(config, config.certname)
if not config.chain_path:
lineage = cert_manager.lineage_for_certname(config, config.certname)
config.chain_path = lineage.chain_path
if oldstyle_enh:
le_client = _init_le_client(config, authenticator=None, installer=installer)
le_client.enhance_config(domains, config.chain_path, ask_redirect=False)
if enhancements.are_requested(config):
enhancements.enable(lineage, domains, installer, config)
le_client = _init_le_client(config, authenticator=None, installer=installer)
le_client.enhance_config(domains, config.chain_path, ask_redirect=False)
def rollback(config, plugins):
@@ -1102,11 +1074,6 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
except errors.PluginSelectionError as e:
return str(e)
# Preflight check for enhancement support by the selected installer
if not enhancements.are_supported(config, installer):
raise errors.NotSupportedError("One ore more of the requested enhancements "
"are not supported by the selected installer")
# TODO: Handle errors from _init_le_client?
le_client = _init_le_client(config, authenticator, installer)
@@ -1125,9 +1092,6 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
_install_cert(config, le_client, domains, new_lineage)
if enhancements.are_requested(config) and new_lineage:
enhancements.enable(new_lineage, domains, installer, config)
if lineage is None or not should_get_cert:
display_ops.success_installation(domains)
else:
@@ -1199,7 +1163,7 @@ def renew_cert(config, plugins, lineage):
# In case of a renewal, reload server to pick up new certificate.
# In principle we could have a configuration option to inhibit this
# from happening.
updater.run_renewal_deployer(config, renewed_lineage, installer)
updater.run_renewal_deployer(renewed_lineage, installer, config)
installer.restart()
notify("new certificate deployed with reload of {0} server; fullchain is {1}".format(
config.installer, lineage.fullchain), pause=False)

View File

@@ -36,7 +36,6 @@ class PluginEntryPoint(object):
"certbot-dns-rfc2136",
"certbot-dns-route53",
"certbot-nginx",
"certbot-postfix",
]
"""Distributions for which prefix will be omitted."""

View File

@@ -1,159 +0,0 @@
"""New interface style Certbot enhancements"""
import abc
import six
from certbot import constants
from acme.magic_typing import Dict, List, Any # pylint: disable=unused-import, no-name-in-module
def enabled_enhancements(config):
"""
Generator to yield the enabled new style enhancements.
:param config: Configuration.
:type config: :class:`certbot.interfaces.IConfig`
"""
for enh in _INDEX:
if getattr(config, enh["cli_dest"]):
yield enh
def are_requested(config):
"""
Checks if one or more of the requested enhancements are those of the new
enhancement interfaces.
:param config: Configuration.
:type config: :class:`certbot.interfaces.IConfig`
"""
return any(enabled_enhancements(config))
def are_supported(config, installer):
"""
Checks that all of the requested enhancements are supported by the
installer.
:param config: Configuration.
:type config: :class:`certbot.interfaces.IConfig`
:param installer: Installer object
:type installer: interfaces.IInstaller
:returns: If all the requested enhancements are supported by the installer
:rtype: bool
"""
for enh in enabled_enhancements(config):
if not isinstance(installer, enh["class"]):
return False
return True
def enable(lineage, domains, installer, config):
"""
Run enable method for each requested enhancement that is supported.
:param lineage: Certificate lineage object
:type lineage: certbot.storage.RenewableCert
:param domains: List of domains in certificate to enhance
:type domains: str
:param installer: Installer object
:type installer: interfaces.IInstaller
:param config: Configuration.
:type config: :class:`certbot.interfaces.IConfig`
"""
for enh in enabled_enhancements(config):
getattr(installer, enh["enable_function"])(lineage, domains)
def populate_cli(add):
"""
Populates the command line flags for certbot.cli.HelpfulParser
:param add: Add function of certbot.cli.HelpfulParser
:type add: func
"""
for enh in _INDEX:
add(enh["cli_groups"], enh["cli_flag"], action=enh["cli_action"],
dest=enh["cli_dest"], default=enh["cli_flag_default"],
help=enh["cli_help"])
@six.add_metaclass(abc.ABCMeta)
class AutoHSTSEnhancement(object):
"""
Enhancement interface that installer plugins can implement in order to
provide functionality that configures the software to have a
'Strict-Transport-Security' with initially low max-age value that will
increase over time.
The plugins implementing new style enhancements are responsible of handling
the saving of configuration checkpoints as well as calling possible restarts
of managed software themselves.
Methods:
enable_autohsts is called when the header is initially installed using a
low max-age value.
update_autohsts is called every time when Certbot is run using 'renew'
verb. The max-age value should be increased over time using this method.
deploy_autohsts is called for every lineage that has had its certificate
renewed. A long HSTS max-age value should be set here, as we should be
confident that the user is able to automatically renew their certificates.
"""
@abc.abstractmethod
def update_autohsts(self, lineage, *args, **kwargs):
"""
Gets called for each lineage every time Certbot is run with 'renew' verb.
Implementation of this method should increase the max-age value.
:param lineage: Certificate lineage object
:type lineage: certbot.storage.RenewableCert
"""
@abc.abstractmethod
def deploy_autohsts(self, lineage, *args, **kwargs):
"""
Gets called for a lineage when its certificate is successfully renewed.
Long max-age value should be set in implementation of this method.
:param lineage: Certificate lineage object
:type lineage: certbot.storage.RenewableCert
"""
@abc.abstractmethod
def enable_autohsts(self, lineage, domains, *args, **kwargs):
"""
Enables the AutoHSTS enhancement, installing
Strict-Transport-Security header with a low initial value to be increased
over the subsequent runs of Certbot renew.
:param lineage: Certificate lineage object
:type lineage: certbot.storage.RenewableCert
:param domains: List of domains in certificate to enhance
:type domains: str
"""
# This is used to configure internal new style enhancements in Certbot. These
# enhancement interfaces need to be defined in this file. Please do not modify
# this list from plugin code.
_INDEX = [
{
"name": "AutoHSTS",
"cli_help": "Gradually increasing max-age value for HTTP Strict Transport "+
"Security security header",
"cli_flag": "--auto-hsts",
"cli_flag_default": constants.CLI_DEFAULTS["auto_hsts"],
"cli_groups": ["security", "enhance"],
"cli_dest": "auto_hsts",
"cli_action": "store_true",
"class": AutoHSTSEnhancement,
"updater_function": "update_autohsts",
"deployer_function": "deploy_autohsts",
"enable_function": "enable_autohsts"
}
] # type: List[Dict[str, Any]]

View File

@@ -1,65 +0,0 @@
"""Tests for new style enhancements"""
import unittest
import mock
from certbot.plugins import enhancements
from certbot.plugins import null
import certbot.tests.util as test_util
class EnhancementTest(test_util.ConfigTestCase):
"""Tests for new style enhancements in certbot.plugins.enhancements"""
def setUp(self):
super(EnhancementTest, self).setUp()
self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)
@test_util.patch_get_utility()
def test_enhancement_enabled_enhancements(self, _):
FAKEINDEX = [
{
"name": "autohsts",
"cli_dest": "auto_hsts",
},
{
"name": "somethingelse",
"cli_dest": "something",
}
]
with mock.patch("certbot.plugins.enhancements._INDEX", FAKEINDEX):
self.config.auto_hsts = True
self.config.something = True
enabled = list(enhancements.enabled_enhancements(self.config))
self.assertEqual(len(enabled), 2)
self.assertTrue([i for i in enabled if i["name"] == "autohsts"])
self.assertTrue([i for i in enabled if i["name"] == "somethingelse"])
def test_are_requested(self):
self.assertEquals(
len([i for i in enhancements.enabled_enhancements(self.config)]), 0)
self.assertFalse(enhancements.are_requested(self.config))
self.config.auto_hsts = True
self.assertEquals(
len([i for i in enhancements.enabled_enhancements(self.config)]), 1)
self.assertTrue(enhancements.are_requested(self.config))
def test_are_supported(self):
self.config.auto_hsts = True
unsupported = null.Installer(self.config, "null")
self.assertTrue(enhancements.are_supported(self.config, self.mockinstaller))
self.assertFalse(enhancements.are_supported(self.config, unsupported))
def test_enable(self):
self.config.auto_hsts = True
domains = ["example.com", "www.example.com"]
lineage = "lineage"
enhancements.enable(lineage, domains, self.mockinstaller, self.config)
self.assertTrue(self.mockinstaller.enable_autohsts.called)
self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0],
(lineage, domains))
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -84,8 +84,7 @@ class PluginStorage(object):
raise errors.PluginStorageError(errmsg)
try:
with os.fdopen(os.open(self._storagepath,
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
0o600), 'w') as fh:
os.O_WRONLY | os.O_CREAT, 0o600), 'w') as fh:
fh.write(serialized)
except IOError as e:
errmsg = "Could not write PluginStorage data to file {0} : {1}".format(

View File

@@ -36,8 +36,7 @@ STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent",
"pre_hook", "post_hook", "tls_sni_01_address",
"http01_address"]
INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"]
BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names", "reuse_key",
"autorenew"]
BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names"]
CONFIG_ITEMS = set(itertools.chain(
BOOL_CONFIG_ITEMS, INT_CONFIG_ITEMS, STR_CONFIG_ITEMS, ('pref_challs',)))
@@ -262,7 +261,7 @@ def should_renew(config, lineage):
if config.renew_by_default:
logger.debug("Auto-renewal forced with --force-renewal...")
return True
if lineage.should_autorenew():
if lineage.should_autorenew(interactive=True):
logger.info("Cert is due for renewal, auto-renewing...")
return True
if config.dry_run:
@@ -299,10 +298,7 @@ def renew_cert(config, domains, le_client, lineage):
_avoid_invalidating_lineage(config, lineage, original_server)
if not domains:
domains = lineage.names()
# The private key is the existing lineage private key if reuse_key is set.
# Otherwise, generate a fresh private key by passing None.
new_key = lineage.privkey if config.reuse_key else None
new_cert, new_chain, new_key, _ = le_client.obtain_certificate(domains, new_key)
new_cert, new_chain, new_key, _ = le_client.obtain_certificate(domains)
if config.dry_run:
logger.debug("Dry run: skipping updating lineage at %s",
os.path.dirname(lineage.cert))
@@ -435,8 +431,8 @@ def handle_renewal_request(config):
renew_skipped.append("%s expires on %s" % (renewal_candidate.fullchain,
expiry.strftime("%Y-%m-%d")))
# Run updater interface methods
updater.run_generic_updaters(lineage_config, renewal_candidate,
plugins)
updater.run_generic_updaters(lineage_config, plugins,
renewal_candidate)
except Exception as e: # pylint: disable=broad-except
# obtain_cert (presumably) encountered an unanticipated problem.

View File

@@ -920,10 +920,10 @@ class RenewableCert(object):
:rtype: bool
"""
return ("autorenew" not in self.configuration["renewalparams"] or
self.configuration["renewalparams"].as_bool("autorenew"))
return ("autorenew" not in self.configuration or
self.configuration.as_bool("autorenew"))
def should_autorenew(self):
def should_autorenew(self, interactive=False):
"""Should we now try to autorenew the most recent cert version?
This is a policy question and does not only depend on whether
@@ -934,12 +934,16 @@ class RenewableCert(object):
Note that this examines the numerically most recent cert version,
not the currently deployed version.
:param bool interactive: set to True to examine the question
regardless of whether the renewal configuration allows
automated renewal (for interactive use). Default False.
:returns: whether an attempt should now be made to autorenew the
most current cert version in this lineage
:rtype: bool
"""
if self.autorenewal_is_enabled():
if interactive or self.autorenewal_is_enabled():
# Consider whether to attempt to autorenew this cert now
# Renewals on the basis of revocation

View File

@@ -95,7 +95,6 @@ class AccountMemoryStorageTest(unittest.TestCase):
class AccountFileStorageTest(test_util.ConfigTestCase):
"""Tests for certbot.account.AccountFileStorage."""
#pylint: disable=too-many-public-methods
def setUp(self):
super(AccountFileStorageTest, self).setUp()
@@ -160,8 +159,7 @@ class AccountFileStorageTest(test_util.ConfigTestCase):
self.assertEqual([], self.storage.find_all())
def test_find_all_load_skips(self):
# pylint: disable=protected-access
self.storage._load_for_server_path = mock.MagicMock(
self.storage.load = mock.MagicMock(
side_effect=["x", errors.AccountStorageError, "z"])
with mock.patch("certbot.account.os.listdir") as mock_listdir:
mock_listdir.return_value = ["x", "y", "z"]
@@ -177,78 +175,6 @@ class AccountFileStorageTest(test_util.ConfigTestCase):
self.assertRaises(errors.AccountStorageError, self.storage.load,
"x" + self.acc.id)
def _set_server(self, server):
self.config.server = server
from certbot.account import AccountFileStorage
self.storage = AccountFileStorage(self.config)
def test_find_all_neither_exists(self):
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
self.assertEqual([], self.storage.find_all())
self.assertEqual([], self.storage.find_all())
self.assertFalse(os.path.islink(self.config.accounts_dir))
def test_find_all_find_before_save(self):
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
self.assertEqual([], self.storage.find_all())
self.storage.save(self.acc, self.mock_client)
self.assertEqual([self.acc], self.storage.find_all())
self.assertEqual([self.acc], self.storage.find_all())
self.assertFalse(os.path.islink(self.config.accounts_dir))
# we shouldn't have created a v1 account
prev_server_path = 'https://acme-staging.api.letsencrypt.org/directory'
self.assertFalse(os.path.isdir(self.config.accounts_dir_for_server_path(prev_server_path)))
def test_find_all_save_before_find(self):
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
self.storage.save(self.acc, self.mock_client)
self.assertEqual([self.acc], self.storage.find_all())
self.assertEqual([self.acc], self.storage.find_all())
self.assertFalse(os.path.islink(self.config.accounts_dir))
self.assertTrue(os.path.isdir(self.config.accounts_dir))
prev_server_path = 'https://acme-staging.api.letsencrypt.org/directory'
self.assertFalse(os.path.isdir(self.config.accounts_dir_for_server_path(prev_server_path)))
def test_find_all_server_downgrade(self):
# don't use v2 accounts with a v1 url
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
self.assertEqual([], self.storage.find_all())
self.storage.save(self.acc, self.mock_client)
self.assertEqual([self.acc], self.storage.find_all())
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
self.assertEqual([], self.storage.find_all())
def test_upgrade_version_staging(self):
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
self.storage.save(self.acc, self.mock_client)
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
self.assertEqual([self.acc], self.storage.find_all())
def test_upgrade_version_production(self):
self._set_server('https://acme-v01.api.letsencrypt.org/directory')
self.storage.save(self.acc, self.mock_client)
self._set_server('https://acme-v02.api.letsencrypt.org/directory')
self.assertEqual([self.acc], self.storage.find_all())
@mock.patch('os.rmdir')
def test_corrupted_account(self, mock_rmdir):
# pylint: disable=protected-access
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
self.storage.save(self.acc, self.mock_client)
mock_rmdir.side_effect = OSError
self.storage._load_for_server_path = mock.MagicMock(
side_effect=errors.AccountStorageError)
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
self.assertEqual([], self.storage.find_all())
def test_upgrade_load(self):
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
self.storage.save(self.acc, self.mock_client)
prev_account = self.storage.load(self.acc.id)
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
account = self.storage.load(self.acc.id)
self.assertEqual(prev_account, account)
def test_load_ioerror(self):
self.storage.save(self.acc, self.mock_client)
mock_open = mock.mock_open()

View File

@@ -1,6 +1,5 @@
"""Tests for certbot.client."""
import os
import platform
import shutil
import tempfile
import unittest
@@ -17,38 +16,6 @@ KEY = test_util.load_vector("rsa512_key.pem")
CSR_SAN = test_util.load_vector("csr-san_512.pem")
class DetermineUserAgentTest(test_util.ConfigTestCase):
"""Tests for certbot.client.determine_user_agent."""
def _call(self):
from certbot.client import determine_user_agent
return determine_user_agent(self.config)
@mock.patch.dict(os.environ, {"CERTBOT_DOCS": "1"})
def test_docs_value(self):
self._test(expect_doc_values=True)
@mock.patch.dict(os.environ, {})
def test_real_values(self):
self._test(expect_doc_values=False)
def _test(self, expect_doc_values):
ua = self._call()
if expect_doc_values:
doc_value_check = self.assertIn
real_value_check = self.assertNotIn
else:
doc_value_check = self.assertNotIn
real_value_check = self.assertIn
doc_value_check("certbot(-auto)", ua)
doc_value_check("OS_NAME OS_VERSION", ua)
doc_value_check("major.minor.patchlevel", ua)
real_value_check(util.get_os_info_ua(), ua)
real_value_check(platform.python_version(), ua)
class RegisterTest(test_util.ConfigTestCase):
"""Tests for certbot.client.register."""

View File

@@ -29,9 +29,7 @@ from certbot import updater
from certbot import util
from certbot.plugins import disco
from certbot.plugins import enhancements
from certbot.plugins import manual
from certbot.plugins import null
import certbot.tests.util as test_util
@@ -54,11 +52,10 @@ class TestHandleIdenticalCerts(unittest.TestCase):
self.assertEqual(ret, ("reinstall", mock_lineage))
class RunTest(test_util.ConfigTestCase):
class RunTest(unittest.TestCase):
"""Tests for certbot.main.run."""
def setUp(self):
super(RunTest, self).setUp()
self.domain = 'example.org'
self.patches = [
mock.patch('certbot.main._get_and_save_cert'),
@@ -108,15 +105,6 @@ class RunTest(test_util.ConfigTestCase):
self._call()
self.mock_success_renewal.assert_called_once_with([self.domain])
@mock.patch('certbot.main.plug_sel.choose_configurator_plugins')
def test_run_enhancement_not_supported(self, mock_choose):
mock_choose.return_value = (null.Installer(self.config, "null"), None)
plugins = disco.PluginsRegistry.find_all()
self.config.auto_hsts = True
self.assertRaises(errors.NotSupportedError,
main.run,
self.config, plugins)
class CertonlyTest(unittest.TestCase):
"""Tests for certbot.main.certonly."""
@@ -761,25 +749,20 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
@mock.patch('certbot.main.plug_sel.pick_installer')
def test_installer_param_error(self, _inst, _rec):
self.assertRaises(errors.ConfigurationError,
self._call,
['install', '--key-path', '/tmp/key_path'])
self.assertRaises(errors.ConfigurationError,
self._call,
['install', '--cert-path', '/tmp/key_path'])
self.assertRaises(errors.ConfigurationError,
self._call,
['install'])
self.assertRaises(errors.ConfigurationError,
self._call,
['install', '--cert-name', 'notfound',
'--key-path', 'invalid'])
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
@mock.patch('certbot.main.plug_sel.pick_installer')
@mock.patch('certbot.cert_manager.get_certnames')
@mock.patch('certbot.main._install_cert')
def test_installer_select_cert(self, mock_inst, mock_getcert, _inst, _rec):
mock_lineage = mock.MagicMock(cert_path="/tmp/cert", chain_path="/tmp/chain",
fullchain_path="/tmp/chain",
key_path="/tmp/privkey")
with mock.patch("certbot.cert_manager.lineage_for_certname") as mock_getlin:
mock_getlin.return_value = mock_lineage
self._call(['install'], mockisfile=True)
self.assertTrue(mock_getcert.called)
self.assertTrue(mock_inst.called)
@mock.patch('certbot.main._report_new_cert')
@mock.patch('certbot.util.exe_exists')
def test_configurator_selection(self, mock_exe_exists, unused_report):
@@ -1043,9 +1026,8 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met
def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None,
args=None, should_renew=True, error_expected=False,
quiet_mode=False, expiry_date=datetime.datetime.now(),
reuse_key=False):
# pylint: disable=too-many-locals,too-many-arguments,too-many-branches
quiet_mode=False, expiry_date=datetime.datetime.now()):
# pylint: disable=too-many-locals,too-many-arguments
cert_path = test_util.vector_path('cert_512.pem')
chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path,
@@ -1095,13 +1077,7 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met
traceback.format_exc())
if should_renew:
if reuse_key:
# The location of the previous live privkey.pem is passed
# to obtain_certificate
mock_client.obtain_certificate.assert_called_once_with(['isnot.org'],
os.path.join(self.config.config_dir, "live/sample-renewal/privkey.pem"))
else:
mock_client.obtain_certificate.assert_called_once_with(['isnot.org'], None)
mock_client.obtain_certificate.assert_called_once_with(['isnot.org'])
else:
self.assertEqual(mock_client.obtain_certificate.call_count, 0)
except:
@@ -1151,17 +1127,6 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met
args = ["renew", "--dry-run", "-tvv"]
self._test_renewal_common(True, [], args=args, should_renew=True)
def test_reuse_key(self):
test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')
args = ["renew", "--dry-run", "--reuse-key"]
self._test_renewal_common(True, [], args=args, should_renew=True, reuse_key=True)
@mock.patch('certbot.storage.RenewableCert.save_successor')
def test_reuse_key_no_dry_run(self, unused_save_successor):
test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')
args = ["renew", "--reuse-key"]
self._test_renewal_common(True, [], args=args, should_renew=True, reuse_key=True)
@mock.patch('certbot.renewal.should_renew')
def test_renew_skips_recent_certs(self, should_renew):
should_renew.return_value = False
@@ -1499,8 +1464,7 @@ class MainTest(test_util.ConfigTestCase): # pylint: disable=too-many-public-met
None, None, None)
with mock.patch('certbot.updater.logger.warning') as mock_log:
self.config.dry_run = False
updater.run_generic_updaters(self.config, None, None)
updater.run_generic_updaters(None, None, None)
self.assertTrue(mock_log.called)
self.assertTrue("Could not choose appropriate plugin for updaters"
in mock_log.call_args[0][0])
@@ -1590,14 +1554,12 @@ class MakeOrVerifyNeededDirs(test_util.ConfigTestCase):
strict=self.config.strict_permissions)
class EnhanceTest(test_util.ConfigTestCase):
class EnhanceTest(unittest.TestCase):
"""Tests for certbot.main.enhance."""
def setUp(self):
super(EnhanceTest, self).setUp()
self.get_utility_patch = test_util.patch_get_utility()
self.mock_get_utility = self.get_utility_patch.start()
self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)
def tearDown(self):
self.get_utility_patch.stop()
@@ -1689,7 +1651,7 @@ class EnhanceTest(test_util.ConfigTestCase):
def test_no_enhancements_defined(self):
self.assertRaises(errors.MisconfigurationError,
self._call, ['enhance', '-a', 'null'])
self._call, ['enhance'])
@mock.patch('certbot.main.plug_sel.choose_configurator_plugins')
@mock.patch('certbot.main.display_ops.choose_values')
@@ -1701,70 +1663,5 @@ class EnhanceTest(test_util.ConfigTestCase):
mock_client = self._call(['enhance', '--hsts'])
self.assertFalse(mock_client.enhance_config.called)
@mock.patch('certbot.cert_manager.lineage_for_certname')
@mock.patch('certbot.main.display_ops.choose_values')
@mock.patch('certbot.main.plug_sel.pick_installer')
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
@test_util.patch_get_utility()
def test_enhancement_enable(self, _, _rec, mock_inst, mock_choose, mock_lineage):
mock_inst.return_value = self.mockinstaller
mock_choose.return_value = ["example.com", "another.tld"]
mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent")
self._call(['enhance', '--auto-hsts'])
self.assertTrue(self.mockinstaller.enable_autohsts.called)
self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0][1],
["example.com", "another.tld"])
@mock.patch('certbot.cert_manager.lineage_for_certname')
@mock.patch('certbot.main.display_ops.choose_values')
@mock.patch('certbot.main.plug_sel.pick_installer')
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
@test_util.patch_get_utility()
def test_enhancement_enable_not_supported(self, _, _rec, mock_inst, mock_choose, mock_lineage):
mock_inst.return_value = null.Installer(self.config, "null")
mock_choose.return_value = ["example.com", "another.tld"]
mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent")
self.assertRaises(
errors.NotSupportedError,
self._call, ['enhance', '--auto-hsts'])
def test_enhancement_enable_conflict(self):
self.assertRaises(
errors.Error,
self._call, ['enhance', '--auto-hsts', '--hsts'])
class InstallTest(test_util.ConfigTestCase):
"""Tests for certbot.main.install."""
def setUp(self):
super(InstallTest, self).setUp()
self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
@mock.patch('certbot.main.plug_sel.pick_installer')
def test_install_enhancement_not_supported(self, mock_inst, _rec):
mock_inst.return_value = null.Installer(self.config, "null")
plugins = disco.PluginsRegistry.find_all()
self.config.auto_hsts = True
self.config.certname = "nonexistent"
self.assertRaises(errors.NotSupportedError,
main.install,
self.config, plugins)
@mock.patch('certbot.main.plug_sel.record_chosen_plugins')
@mock.patch('certbot.main.plug_sel.pick_installer')
def test_install_enhancement_no_certname(self, mock_inst, _rec):
mock_inst.return_value = self.mockinstaller
plugins = disco.PluginsRegistry.find_all()
self.config.auto_hsts = True
self.config.certname = None
self.config.key_path = "/tmp/nonexistent"
self.config.cert_path = "/tmp/nonexistent"
self.assertRaises(errors.ConfigurationError,
main.install,
self.config, plugins)
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -6,117 +6,70 @@ from certbot import interfaces
from certbot import main
from certbot import updater
from certbot.plugins import enhancements
import certbot.tests.util as test_util
class RenewUpdaterTest(test_util.ConfigTestCase):
class RenewUpdaterTest(unittest.TestCase):
"""Tests for interfaces.RenewDeployer and interfaces.GenericUpdater"""
def setUp(self):
super(RenewUpdaterTest, self).setUp()
self.generic_updater = mock.MagicMock(spec=interfaces.GenericUpdater)
self.generic_updater.restart = mock.MagicMock()
self.renew_deployer = mock.MagicMock(spec=interfaces.RenewDeployer)
self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)
class MockInstallerGenericUpdater(interfaces.GenericUpdater):
"""Mock class that implements GenericUpdater"""
def __init__(self, *args, **kwargs):
# pylint: disable=unused-argument
self.restart = mock.MagicMock()
self.callcounter = mock.MagicMock()
def generic_updates(self, domain, *args, **kwargs):
self.callcounter(*args, **kwargs)
class MockInstallerRenewDeployer(interfaces.RenewDeployer):
"""Mock class that implements RenewDeployer"""
def __init__(self, *args, **kwargs):
# pylint: disable=unused-argument
self.callcounter = mock.MagicMock()
def renew_deploy(self, lineage, *args, **kwargs):
self.callcounter(*args, **kwargs)
self.generic_updater = MockInstallerGenericUpdater()
self.renew_deployer = MockInstallerRenewDeployer()
def get_config(self, args):
"""Get mock config from dict of parameters"""
config = mock.MagicMock()
for key in args.keys():
config.__dict__[key] = args[key]
return config
@mock.patch('certbot.main._get_and_save_cert')
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
@test_util.patch_get_utility()
def test_server_updates(self, _, mock_select, mock_getsave):
mock_getsave.return_value = mock.MagicMock()
config = self.get_config({"disable_renew_updates": False})
lineage = mock.MagicMock()
lineage.names.return_value = ['firstdomain', 'seconddomain']
mock_getsave.return_value = lineage
mock_generic_updater = self.generic_updater
# Generic Updater
mock_select.return_value = (mock_generic_updater, None)
with mock.patch('certbot.main._init_le_client'):
main.renew_cert(self.config, None, mock.MagicMock())
main.renew_cert(config, None, mock.MagicMock())
self.assertTrue(mock_generic_updater.restart.called)
mock_generic_updater.restart.reset_mock()
mock_generic_updater.generic_updates.reset_mock()
updater.run_generic_updaters(self.config, mock.MagicMock(), None)
self.assertEqual(mock_generic_updater.generic_updates.call_count, 1)
mock_generic_updater.callcounter.reset_mock()
updater.run_generic_updaters(config, None, lineage)
self.assertEqual(mock_generic_updater.callcounter.call_count, 2)
self.assertFalse(mock_generic_updater.restart.called)
def test_renew_deployer(self):
config = self.get_config({"disable_renew_updates": False})
lineage = mock.MagicMock()
lineage.names.return_value = ['firstdomain', 'seconddomain']
mock_deployer = self.renew_deployer
updater.run_renewal_deployer(self.config, lineage, mock_deployer)
self.assertTrue(mock_deployer.renew_deploy.called_with(lineage))
@mock.patch("certbot.updater.logger.debug")
def test_updater_skip_dry_run(self, mock_log):
self.config.dry_run = True
updater.run_generic_updaters(self.config, None, None)
self.assertTrue(mock_log.called)
self.assertEquals(mock_log.call_args[0][0],
"Skipping updaters in dry-run mode.")
@mock.patch("certbot.updater.logger.debug")
def test_deployer_skip_dry_run(self, mock_log):
self.config.dry_run = True
updater.run_renewal_deployer(self.config, None, None)
self.assertTrue(mock_log.called)
self.assertEquals(mock_log.call_args[0][0],
"Skipping renewal deployer in dry-run mode.")
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
def test_enhancement_updates(self, mock_select):
mock_select.return_value = (self.mockinstaller, None)
updater.run_generic_updaters(self.config, mock.MagicMock(), None)
self.assertTrue(self.mockinstaller.update_autohsts.called)
self.assertEqual(self.mockinstaller.update_autohsts.call_count, 1)
def test_enhancement_deployer(self):
updater.run_renewal_deployer(self.config, mock.MagicMock(),
self.mockinstaller)
self.assertTrue(self.mockinstaller.deploy_autohsts.called)
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
def test_enhancement_updates_not_called(self, mock_select):
self.config.disable_renew_updates = True
mock_select.return_value = (self.mockinstaller, None)
updater.run_generic_updaters(self.config, mock.MagicMock(), None)
self.assertFalse(self.mockinstaller.update_autohsts.called)
def test_enhancement_deployer_not_called(self):
self.config.disable_renew_updates = True
updater.run_renewal_deployer(self.config, mock.MagicMock(),
self.mockinstaller)
self.assertFalse(self.mockinstaller.deploy_autohsts.called)
@mock.patch('certbot.plugins.selection.choose_configurator_plugins')
def test_enhancement_no_updater(self, mock_select):
FAKEINDEX = [
{
"name": "Test",
"class": enhancements.AutoHSTSEnhancement,
"updater_function": None,
"deployer_function": "deploy_autohsts",
"enable_function": "enable_autohsts"
}
]
mock_select.return_value = (self.mockinstaller, None)
with mock.patch("certbot.plugins.enhancements._INDEX", FAKEINDEX):
updater.run_generic_updaters(self.config, mock.MagicMock(), None)
self.assertFalse(self.mockinstaller.update_autohsts.called)
def test_enhancement_no_deployer(self):
FAKEINDEX = [
{
"name": "Test",
"class": enhancements.AutoHSTSEnhancement,
"updater_function": "deploy_autohsts",
"deployer_function": None,
"enable_function": "enable_autohsts"
}
]
with mock.patch("certbot.plugins.enhancements._INDEX", FAKEINDEX):
updater.run_renewal_deployer(self.config, mock.MagicMock(),
self.mockinstaller)
self.assertFalse(self.mockinstaller.deploy_autohsts.called)
updater.run_renewal_deployer(lineage, mock_deployer, config)
self.assertTrue(mock_deployer.callcounter.called_with(lineage))
if __name__ == '__main__':

View File

@@ -383,9 +383,8 @@ class RenewableCertTests(BaseRenewableCertTest):
os.unlink(self.test_rc.cert)
self.assertRaises(errors.CertStorageError, self.test_rc.names)
@mock.patch("certbot.storage.cli")
@mock.patch("certbot.storage.datetime")
def test_time_interval_judgments(self, mock_datetime, mock_cli):
def test_time_interval_judgments(self, mock_datetime):
"""Test should_autodeploy() and should_autorenew() on the basis
of expiry time windows."""
test_cert = test_util.load_vector("cert_512.pem")
@@ -400,8 +399,6 @@ class RenewableCertTests(BaseRenewableCertTest):
f.write(test_cert)
mock_datetime.timedelta = datetime.timedelta
mock_cli.set_by_cli.return_value = False
self.test_rc.configuration["renewalparams"] = {}
for (current_time, interval, result) in [
# 2014-12-13 12:00:00+00:00 (about 5 days prior to expiry)
@@ -454,25 +451,22 @@ class RenewableCertTests(BaseRenewableCertTest):
self.assertFalse(self.test_rc.should_autodeploy())
def test_autorenewal_is_enabled(self):
self.test_rc.configuration["renewalparams"] = {}
self.assertTrue(self.test_rc.autorenewal_is_enabled())
self.test_rc.configuration["renewalparams"]["autorenew"] = "True"
self.test_rc.configuration["autorenew"] = "1"
self.assertTrue(self.test_rc.autorenewal_is_enabled())
self.test_rc.configuration["renewalparams"]["autorenew"] = "False"
self.test_rc.configuration["autorenew"] = "0"
self.assertFalse(self.test_rc.autorenewal_is_enabled())
@mock.patch("certbot.storage.cli")
@mock.patch("certbot.storage.RenewableCert.ocsp_revoked")
def test_should_autorenew(self, mock_ocsp, mock_cli):
def test_should_autorenew(self, mock_ocsp):
"""Test should_autorenew on the basis of reasons other than
expiry time window."""
# pylint: disable=too-many-statements
mock_cli.set_by_cli.return_value = False
# Autorenewal turned off
self.test_rc.configuration["renewalparams"] = {"autorenew": "False"}
self.test_rc.configuration["autorenew"] = "0"
self.assertFalse(self.test_rc.should_autorenew())
self.test_rc.configuration["renewalparams"]["autorenew"] = "True"
self.test_rc.configuration["autorenew"] = "1"
for kind in ALL_FOUR:
self._write_out_kind(kind, 12)
# Mandatory renewal on the basis of OCSP revocation

View File

@@ -5,28 +5,24 @@ from certbot import errors
from certbot import interfaces
from certbot.plugins import selection as plug_sel
import certbot.plugins.enhancements as enhancements
logger = logging.getLogger(__name__)
def run_generic_updaters(config, lineage, plugins):
def run_generic_updaters(config, plugins, lineage):
"""Run updaters that the plugin supports
:param config: Configuration object
:type config: interfaces.IConfig
:param lineage: Certificate lineage object
:type lineage: storage.RenewableCert
:param plugins: List of plugins
:type plugins: `list` of `str`
:param lineage: Certificate lineage object
:type lineage: storage.RenewableCert
:returns: `None`
:rtype: None
"""
if config.dry_run:
logger.debug("Skipping updaters in dry-run mode.")
return
try:
# installers are used in auth mode to determine domain names
installer, _ = plug_sel.choose_configurator_plugins(config, plugins, "certonly")
@@ -34,15 +30,11 @@ def run_generic_updaters(config, lineage, plugins):
logger.warning("Could not choose appropriate plugin for updaters: %s", e)
return
_run_updaters(lineage, installer, config)
_run_enhancement_updaters(lineage, installer, config)
def run_renewal_deployer(config, lineage, installer):
def run_renewal_deployer(lineage, installer, config):
"""Helper function to run deployer interface method if supported by the used
installer plugin.
:param config: Configuration object
:type config: interfaces.IConfig
:param lineage: Certificate lineage object
:type lineage: storage.RenewableCert
@@ -52,14 +44,9 @@ def run_renewal_deployer(config, lineage, installer):
:returns: `None`
:rtype: None
"""
if config.dry_run:
logger.debug("Skipping renewal deployer in dry-run mode.")
return
if not config.disable_renew_updates and isinstance(installer,
interfaces.RenewDeployer):
installer.renew_deploy(lineage)
_run_enhancement_deployers(lineage, installer, config)
def _run_updaters(lineage, installer, config):
"""Helper function to run the updater interface methods if supported by the
@@ -74,49 +61,7 @@ def _run_updaters(lineage, installer, config):
:returns: `None`
:rtype: None
"""
if not config.disable_renew_updates:
if isinstance(installer, interfaces.GenericUpdater):
installer.generic_updates(lineage)
def _run_enhancement_updaters(lineage, installer, config):
"""Iterates through known enhancement interfaces. If the installer implements
an enhancement interface and the enhance interface has an updater method, the
updater method gets run.
:param lineage: Certificate lineage object
:type lineage: storage.RenewableCert
:param installer: Installer object
:type installer: interfaces.IInstaller
:param config: Configuration object
:type config: interfaces.IConfig
"""
if config.disable_renew_updates:
return
for enh in enhancements._INDEX: # pylint: disable=protected-access
if isinstance(installer, enh["class"]) and enh["updater_function"]:
getattr(installer, enh["updater_function"])(lineage)
def _run_enhancement_deployers(lineage, installer, config):
"""Iterates through known enhancement interfaces. If the installer implements
an enhancement interface and the enhance interface has an deployer method, the
deployer method gets run.
:param lineage: Certificate lineage object
:type lineage: storage.RenewableCert
:param installer: Installer object
:type installer: interfaces.IInstaller
:param config: Configuration object
:type config: interfaces.IConfig
"""
if config.disable_renew_updates:
return
for enh in enhancements._INDEX: # pylint: disable=protected-access
if isinstance(installer, enh["class"]) and enh["deployer_function"]:
getattr(installer, enh["deployer_function"])(lineage)
for domain in lineage.names():
if not config.disable_renew_updates:
if isinstance(installer, interfaces.GenericUpdater):
installer.generic_updates(domain)

View File

@@ -108,12 +108,12 @@ optional arguments:
case, and to know when to deprecate support for past
Python versions and flags. If you wish to hide this
information from the Let's Encrypt server, set this to
"". (default: CertbotACMEClient/0.25.1
(certbot(-auto); OS_NAME OS_VERSION) Authenticator/XXX
Installer/YYY (SUBCOMMAND; flags: FLAGS)
Py/major.minor.patchlevel). The flags encoded in the
user agent are: --duplicate, --force-renew, --allow-
subset-of-names, -n, and whether any hooks are set.
"". (default: CertbotACMEClient/0.24.0 (certbot;
darwin 10.13.4) Authenticator/XXX Installer/YYY
(SUBCOMMAND; flags: FLAGS) Py/2.7.14). The flags
encoded in the user agent are: --duplicate, --force-
renew, --allow-subset-of-names, -n, and whether any
hooks are set.
--user-agent-comment USER_AGENT_COMMENT
Add a comment to the default user agent string. May be
used when repackaging Certbot or calling it from
@@ -143,8 +143,6 @@ automation:
certificate name but does not match the requested
domains, renew it now, regardless of whether it is
near expiry. (default: False)
--reuse-key When renewing, use the same private key as the
existing certificate. (default: False)
--allow-subset-of-names
When performing domain validation, do not consider it
a failure if authorizations can not be obtained for a
@@ -321,13 +319,6 @@ renew:
disable it. (default: False)
--no-directory-hooks Disable running executables found in Certbot's hook
directories during renewal. (default: False)
--disable-renew-updates
Disable automatic updates to your server configuration
that would otherwise be done by the selected installer
plugin, and triggered when the user executes "certbot
renew", regardless of if the certificate is renewed.
This setting does not apply to important TLS
configuration updates. (default: False)
certificates:
List certificates managed by Certbot
@@ -369,9 +360,8 @@ register:
e-mail address, should be updated, rather than
registering a new account. (default: False)
-m EMAIL, --email EMAIL
Email used for registration and recovery contact. Use
comma to register multiple emails, ex:
u1@example.com,u2@example.com. (default: Ask).
Email used for registration and recovery contact.
(default: Ask)
--eff-email Share your e-mail address with EFF (default: None)
--no-eff-email Don't share your e-mail address with EFF (default:
None)
@@ -409,7 +399,7 @@ update_symlinks:
changed them by hand or edited a renewal configuration file
enhance:
Helps to harden the TLS configuration by adding security enhancements to
Helps to harden the TLS configration by adding security enhancements to
already existing configuration.
plugins:
@@ -482,9 +472,9 @@ apache:
/etc/apache2/other)
--apache-handle-modules APACHE_HANDLE_MODULES
Let installer handle enabling required modules for
you. (Only Ubuntu/Debian currently) (default: False)
you.(Only Ubuntu/Debian currently) (default: False)
--apache-handle-sites APACHE_HANDLE_SITES
Let installer handle enabling sites for you. (Only
Let installer handle enabling sites for you.(Only
Ubuntu/Debian currently) (default: False)
certbot-route53:auth:
@@ -638,8 +628,7 @@ nginx:
Nginx Web Server plugin - Alpha
--nginx-server-root NGINX_SERVER_ROOT
Nginx server root directory. (default: /etc/nginx or
/usr/local/etc/nginx)
Nginx server root directory. (default: /etc/nginx)
--nginx-ctl NGINX_CTL
Path to the 'nginx' binary, used for 'configtest' and
retrieving nginx version number. (default: nginx)

View File

@@ -312,40 +312,6 @@ Please:
.. _PEP 8 - Style Guide for Python Code:
https://www.python.org/dev/peps/pep-0008
Mypy type annotations
=====================
Certbot uses the `mypy`_ static type checker. Python 3 natively supports official type annotations,
which can then be tested for consistency using mypy. Python 2 doesnt, but type annotations can
be `added in comments`_. Mypy does some type checks even without type annotations; we can find
bugs in Certbot even without a fully annotated codebase.
Certbot supports both Python 2 and 3, so were using Python 2-style annotations.
Zulip wrote a `great guide`_ to using mypy. Its useful, but you dont have to read the whole thing
to start contributing to Certbot.
To run mypy on Certbot, use ``tox -e mypy`` on a machine that has Python 3 installed.
Note that instead of just importing ``typing``, due to packaging issues, in Certbot we import from
``acme.magic_typing`` and have to add some comments for pylint like this:
.. code-block:: python
from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
Also note that OpenSSL, which we rely on, has type definitions for crypto but not SSL. We use both.
Those imports should look like this:
.. code-block:: python
from OpenSSL import crypto
from OpenSSL import SSL # type: ignore # https://github.com/python/typeshed/issues/2052
.. _mypy: https://mypy.readthedocs.io
.. _added in comments: https://mypy.readthedocs.io/en/latest/cheat_sheet.html
.. _great guide: https://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/
Submitting a pull request
=========================

View File

@@ -454,12 +454,6 @@ Renewing certificates
days). Make sure you renew the certificates at least once in 3
months.
.. seealso:: Many of the certbot clients obtained through a
distribution come with automatic renewal out of the box,
such as Debian and Ubuntu versions installed through `apt`,
CentOS/RHEL 7 through EPEL, etc. See `Automated Renewals`_
for more details.
As of version 0.10.0, Certbot supports a ``renew`` action to check
all installed certificates for impending expiry and attempt to renew
them. The simplest form is simply
@@ -566,6 +560,12 @@ can run on a regular basis, like every week or every day). In that case,
you are likely to want to use the ``-q`` or ``--quiet`` quiet flag to
silence all output except errors.
.. seealso:: Many of the certbot clients obtained through a
distribution come with automatic renewal out of the box,
such as Debian and Ubuntu versions installed through `apt`,
CentOS/RHEL 7 through EPEL, etc. See `Automated Renewals`_
for more details.
If you are manually renewing all of your certificates, the
``--force-renewal`` flag may be helpful; it causes the expiration time of
the certificate(s) to be ignored when considering renewal, and attempts to

View File

@@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
fi
VENV_BIN="$VENV_PATH/bin"
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
LE_AUTO_VERSION="0.25.1"
LE_AUTO_VERSION="0.24.0"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@@ -1055,11 +1055,9 @@ cffi==1.10.0 \
--hash=sha256:5576644b859197da7bbd8f8c7c2fb5dcc6cd505cadb42992d5f104c013f8a214 \
--hash=sha256:b3b02911eb1f6ada203b0763ba924234629b51586f72a21faacc638269f4ced5
ConfigArgParse==0.12.0 \
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339 \
--no-binary ConfigArgParse
--hash=sha256:28cd7d67669651f2a4518367838c49539457504584a139709b2b8f6c208ef339
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 \
--no-binary configobj
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
cryptography==2.0.2 \
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
@@ -1114,8 +1112,7 @@ mock==1.3.0 \
--hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \
--hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6
ordereddict==1.1 \
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f \
--no-binary ordereddict
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
packaging==16.8 \
--hash=sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388 \
--hash=sha256:5d50835fdf0a7edf0b55e311b7c887786504efea1177abd7e69329a8e5ea619e
@@ -1141,8 +1138,7 @@ pyRFC3339==1.0 \
--hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
python-augeas==0.5.0 \
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 \
--no-binary python-augeas
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
pytz==2015.7 \
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
@@ -1170,11 +1166,9 @@ unittest2==1.1.0 \
--hash=sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8 \
--hash=sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579
zope.component==4.2.2 \
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a \
--no-binary zope.component
--hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a
zope.event==4.1.0 \
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786 \
--no-binary zope.event
--hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786
zope.interface==4.1.3 \
--hash=sha256:f07b631f7a601cd8cbd3332d54f43142c7088a83299f859356f08d1d4d4259b3 \
--hash=sha256:de5cca083b9439d8002fb76bbe6b4998c5a5a721fab25b84298967f002df4c94 \
@@ -1193,9 +1187,6 @@ zope.interface==4.1.3 \
--hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \
--hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \
--hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392
requests-toolbelt==0.8.0 \
--hash=sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237 \
--hash=sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5
# Contains the requirements for the letsencrypt package.
#
@@ -1208,18 +1199,18 @@ letsencrypt==0.7.0 \
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
certbot==0.25.1 \
--hash=sha256:01689015364685fef3f1e1fb7832ba84eb3b0aa85bc5a71c96661f6d4c59981f \
--hash=sha256:5c23e5186133bb1afd805be5e0cd2fb7b95862a8b0459c9ecad4ae60f933e54e
acme==0.25.1 \
--hash=sha256:26e641a01536705fe5f12d856703b8ef06e5a07981a7b6379d2771dcdb69a742 \
--hash=sha256:47b5f3f73d69b7b1d13f918aa2cd75a8093069a68becf4af38e428e4613b2734
certbot-apache==0.25.1 \
--hash=sha256:a28b7c152cc11474bef5b5e7967aaea42b2c0aaf86fd82ee4082713d33cee5a9 \
--hash=sha256:ed012465617073a0f1057fe854dc8d1eb6d2dd7ede1fb2eee765129fed2a095a
certbot-nginx==0.25.1 \
--hash=sha256:83f82c3ba08c0b1d4bf449ac24018e8e7dd34a6248d35466f2de7da1cd312e15 \
--hash=sha256:68f98b41c54e0bf4218ef293079597176617bee3837ae3aa6528ce2ff0bf4f9c
certbot==0.24.0 \
--hash=sha256:a3fc41fde4f0dbb35f7ebec2f9e00339580b3f4298850411eac0719223073b27 \
--hash=sha256:a072d4528bb3ac4184f5c961a96931795ddfe4b7cb0f3a98954bdd4cce5f6d70
acme==0.24.0 \
--hash=sha256:b92b16102051f447abb52917638fbfb34b646ac07267fee85961b360a0149e32 \
--hash=sha256:d655e0627c0830114ab3f6732d8bf2f4a2c36f602e0cde10988684e229b501cb
certbot-apache==0.24.0 \
--hash=sha256:fe54db3e7e09ffe1139041c23ff5123e80aa1526d6fcd40b2a593d005cfcf152 \
--hash=sha256:686c6c0af5ae8d06e37cc762de7ffa0dc5c3b1ba06ff7653ef61713fa016f891
certbot-nginx==0.24.0 \
--hash=sha256:d44c419f72c2cc30de3b138a2cf92e0531696dcb048f287036e229dce2131c00 \
--hash=sha256:3283d1db057261f05537fa408baee20e0ab9e81c3d55cfba70afe3805cd6f941
UNLIKELY_EOF
# -------------------------------------------------------------------------

View File

@@ -1,11 +1,11 @@
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAlsgc/cACgkQTRfJlc2X
dfLjBgf/bHZn/q+Dqn34uBXHymRSce7UxQn17izcKAt7hZBl4j4sebQ9+0jjuNur
zrW8b0XJ0PsI10GG9qHR3ajC+04pWfRritnK1g4Ycb/pDcUkWo+8uRwr7skAVcvC
oa8ToBS3iUbd3csFl1mu1BGACUHLvVs2cYdDtMuJj8wjsVZ7KnWBGKULAskwmU4Z
VVUxeUrG9f+2kT35meEJUk91FS+4tmqNIVsVlBzf0Q0ZU1iQnV56dMwTqFRzdDJ2
DBecE0GwuYnKXo2I7kIYaqACQmk9YFh55Sh0K9PbQxyv7YEZXZtkcdqFqyhxy3Nh
EJ2kurFaM3/VmLljc/rW8QW8B3QNbw==
=pkDz
iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAlro/1AACgkQTRfJlc2X
dfLm5ggAxCrWU9dmYZKllcFzp7TFOdRap0pmarfL4gwSYj7B/bSceD7ysOyoQ8Ra
7UHuZKAQyurZn1seN49d88Kgor9KWZQ1jZiGkfiEpp8qAkdWzFR8UqYa2/CZtk2l
bExm8YQDwhuKvCObGLDGi3ydcIQpfg/rsBkSTphKYXN/Zebx9mAelZN4CgGRy03Y
3z2UqqnyqFPAg4wUGcNfCgUEbJ5bUPr733vQzjBS2IVUbDbu06/1Y8oYzurezXNS
6lEyvTfC5G8RGlSWupNu7yWviD14M4LnAo6WXWEVH+C+ssJaPrZVhZ6KfEt/Erg3
k06WZSPDCtOm5EfhDm0Rumqm1owA2g==
=Bc4G
-----END PGP SIGNATURE-----

View File

@@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
fi
VENV_BIN="$VENV_PATH/bin"
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
LE_AUTO_VERSION="0.26.0.dev0"
LE_AUTO_VERSION="0.25.0.dev0"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@@ -1060,26 +1060,37 @@ ConfigArgParse==0.12.0 \
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 \
--no-binary configobj
cryptography==2.2.2 \
--hash=sha256:3f3b65d5a16e6b52fba63dc860b62ca9832f51f1a2ae5083c78b6840275f12dd \
--hash=sha256:5251e7de0de66810833606439ca65c9b9e45da62196b0c88bfadf27740aac09f \
--hash=sha256:551a3abfe0c8c6833df4192a63371aa2ff43afd8f570ed345d31f251d78e7e04 \
--hash=sha256:5cb990056b7cadcca26813311187ad751ea644712022a3976443691168781b6f \
--hash=sha256:60bda7f12ecb828358be53095fc9c6edda7de8f1ef571f96c00b2363643fa3cd \
--hash=sha256:64b5c67acc9a7c83fbb4b69166f3105a0ab722d27934fac2cb26456718eec2ba \
--hash=sha256:6fef51ec447fe9f8351894024e94736862900d3a9aa2961528e602eb65c92bdb \
--hash=sha256:77d0ad229d47a6e0272d00f6bf8ac06ce14715a9fd02c9a97f5a2869aab3ccb2 \
--hash=sha256:808fe471b1a6b777f026f7dc7bd9a4959da4bfab64972f2bbe91e22527c1c037 \
--hash=sha256:9b62fb4d18529c84b961efd9187fecbb48e89aa1a0f9f4161c61b7fc42a101bd \
--hash=sha256:9e5bed45ec6b4f828866ac6a6bedf08388ffcfa68abe9e94b34bb40977aba531 \
--hash=sha256:9fc295bf69130a342e7a19a39d7bbeb15c0bcaabc7382ec33ef3b2b7d18d2f63 \
--hash=sha256:abd070b5849ed64e6d349199bef955ee0ad99aefbad792f0c587f8effa681a5e \
--hash=sha256:ba6a774749b6e510cffc2fb98535f717e0e5fd91c7c99a61d223293df79ab351 \
--hash=sha256:c332118647f084c983c6a3e1dba0f3bcb051f69d12baccac68db8d62d177eb8a \
--hash=sha256:d6f46e862ee36df81e6342c2177ba84e70f722d9dc9c6c394f9f1f434c4a5563 \
--hash=sha256:db6013746f73bf8edd9c3d1d3f94db635b9422f503db3fc5ef105233d4c011ab \
--hash=sha256:f57008eaff597c69cf692c3518f6d4800f0309253bb138b526a37fe9ef0c7471 \
--hash=sha256:f6c821ac253c19f2ad4c8691633ae1d1a17f120d5b01ea1d256d7b602bc59887
cryptography==2.0.2 \
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
--hash=sha256:da9291502cbc87dc0284a20c56876e4d2e68deac61cc43df4aec934e44ca97b1 \
--hash=sha256:0954f8813095f581669330e0a2d5e726c33ac7f450c1458fac58bab54595e516 \
--hash=sha256:d68b0cc40a8432ed3fc84876c519de704d6001800ec22b136e75ae841910c45b \
--hash=sha256:2f8ad9580ab4da645cfea52a91d2da99a49a1e76616d8be68441a986fad652b0 \
--hash=sha256:cc00b4511294f5f6b65c4e77a1a9c62f52490a63d2c120f3872176b40a82351e \
--hash=sha256:cf896020f6a9f095a547b3d672c8db1ef2ed71fca11250731fa1d4a4cb8b1590 \
--hash=sha256:e0fdb8322206fa02aa38f71519ff75dce2eb481b7e1110e2936795cb376bb6ee \
--hash=sha256:277538466657ca5d6637f80be100242f9831d75138b788d718edd3aab34621f8 \
--hash=sha256:2c77eb0560f54ce654ab82d6b2a64327a71ee969b29022bf9746ca311c9f5069 \
--hash=sha256:755a7853b679e79d0a799351c092a9b0271f95ff54c8dd8823d8b527a2926a86 \
--hash=sha256:77197a2d525e761cdd4c771180b4bd0d80703654c6385e4311cbbbe2beb56fa1 \
--hash=sha256:eb8bb79d0ab00c931c8333b745f06fec481a51c52d70acd4ee95d6093ba5c386 \
--hash=sha256:131f61de82ef28f3e20beb4bfc24f9692d28cecfd704e20e6c7f070f7793013a \
--hash=sha256:ac35435974b2e27cd4520f29c191d7da36f4189aa3264e52c4c6c6d089ab6142 \
--hash=sha256:04b6ea99daa2a8460728794213d76d45ad58ea247dc7e7ff148d7dd726e87863 \
--hash=sha256:2b9442f8b4c3d575f6cc3db0e856034e0f5a9d55ecd636f52d8c496795b26952 \
--hash=sha256:b3d3b3ecba1fe1bdb6f180770a137f877c8f07571f7b2934bb269475bcf0e5e8 \
--hash=sha256:670a58c0d75cb0e78e73dd003bd96d4440bbb1f2bc041dcf7b81767ca4fb0ce9 \
--hash=sha256:5af84d23bdb86b5e90aca263df1424b43f1748480bfcde3ac2a3cbe622612468 \
--hash=sha256:ba22e8eefabdd7aca37d0c0c00d2274000d2cebb5cce9e5a710cb55bf8797b31 \
--hash=sha256:b798b22fa7e92b439547323b8b719d217f1e1b7677585cfeeedf3b55c70bb7fb \
--hash=sha256:59cff28af8cce96cb7e94a459726e1d88f6f5fa75097f9dcbebd99118d64ea4c \
--hash=sha256:fe859e445abc9ba9e97950ddafb904e23234c4ecb76b0fae6c86e80592ce464a \
--hash=sha256:655f3c474067f1e277430f23cc0549f0b1dc99b82aec6e53f80b9b2db7f76f11 \
--hash=sha256:0ebc2be053c9a03a2f3e20a466e87bf12a51586b3c79bd2a22171b073a805346 \
--hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \
--hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \
--hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab
enum34==1.1.2 ; python_version < '3.4' \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
@@ -1197,18 +1208,18 @@ letsencrypt==0.7.0 \
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
certbot==0.25.1 \
--hash=sha256:01689015364685fef3f1e1fb7832ba84eb3b0aa85bc5a71c96661f6d4c59981f \
--hash=sha256:5c23e5186133bb1afd805be5e0cd2fb7b95862a8b0459c9ecad4ae60f933e54e
acme==0.25.1 \
--hash=sha256:26e641a01536705fe5f12d856703b8ef06e5a07981a7b6379d2771dcdb69a742 \
--hash=sha256:47b5f3f73d69b7b1d13f918aa2cd75a8093069a68becf4af38e428e4613b2734
certbot-apache==0.25.1 \
--hash=sha256:a28b7c152cc11474bef5b5e7967aaea42b2c0aaf86fd82ee4082713d33cee5a9 \
--hash=sha256:ed012465617073a0f1057fe854dc8d1eb6d2dd7ede1fb2eee765129fed2a095a
certbot-nginx==0.25.1 \
--hash=sha256:83f82c3ba08c0b1d4bf449ac24018e8e7dd34a6248d35466f2de7da1cd312e15 \
--hash=sha256:68f98b41c54e0bf4218ef293079597176617bee3837ae3aa6528ce2ff0bf4f9c
certbot==0.24.0 \
--hash=sha256:a3fc41fde4f0dbb35f7ebec2f9e00339580b3f4298850411eac0719223073b27 \
--hash=sha256:a072d4528bb3ac4184f5c961a96931795ddfe4b7cb0f3a98954bdd4cce5f6d70
acme==0.24.0 \
--hash=sha256:b92b16102051f447abb52917638fbfb34b646ac07267fee85961b360a0149e32 \
--hash=sha256:d655e0627c0830114ab3f6732d8bf2f4a2c36f602e0cde10988684e229b501cb
certbot-apache==0.24.0 \
--hash=sha256:fe54db3e7e09ffe1139041c23ff5123e80aa1526d6fcd40b2a593d005cfcf152 \
--hash=sha256:686c6c0af5ae8d06e37cc762de7ffa0dc5c3b1ba06ff7653ef61713fa016f891
certbot-nginx==0.24.0 \
--hash=sha256:d44c419f72c2cc30de3b138a2cf92e0531696dcb048f287036e229dce2131c00 \
--hash=sha256:3283d1db057261f05537fa408baee20e0ab9e81c3d55cfba70afe3805cd6f941
UNLIKELY_EOF
# -------------------------------------------------------------------------

View File

@@ -1,12 +1,12 @@
certbot==0.25.1 \
--hash=sha256:01689015364685fef3f1e1fb7832ba84eb3b0aa85bc5a71c96661f6d4c59981f \
--hash=sha256:5c23e5186133bb1afd805be5e0cd2fb7b95862a8b0459c9ecad4ae60f933e54e
acme==0.25.1 \
--hash=sha256:26e641a01536705fe5f12d856703b8ef06e5a07981a7b6379d2771dcdb69a742 \
--hash=sha256:47b5f3f73d69b7b1d13f918aa2cd75a8093069a68becf4af38e428e4613b2734
certbot-apache==0.25.1 \
--hash=sha256:a28b7c152cc11474bef5b5e7967aaea42b2c0aaf86fd82ee4082713d33cee5a9 \
--hash=sha256:ed012465617073a0f1057fe854dc8d1eb6d2dd7ede1fb2eee765129fed2a095a
certbot-nginx==0.25.1 \
--hash=sha256:83f82c3ba08c0b1d4bf449ac24018e8e7dd34a6248d35466f2de7da1cd312e15 \
--hash=sha256:68f98b41c54e0bf4218ef293079597176617bee3837ae3aa6528ce2ff0bf4f9c
certbot==0.24.0 \
--hash=sha256:a3fc41fde4f0dbb35f7ebec2f9e00339580b3f4298850411eac0719223073b27 \
--hash=sha256:a072d4528bb3ac4184f5c961a96931795ddfe4b7cb0f3a98954bdd4cce5f6d70
acme==0.24.0 \
--hash=sha256:b92b16102051f447abb52917638fbfb34b646ac07267fee85961b360a0149e32 \
--hash=sha256:d655e0627c0830114ab3f6732d8bf2f4a2c36f602e0cde10988684e229b501cb
certbot-apache==0.24.0 \
--hash=sha256:fe54db3e7e09ffe1139041c23ff5123e80aa1526d6fcd40b2a593d005cfcf152 \
--hash=sha256:686c6c0af5ae8d06e37cc762de7ffa0dc5c3b1ba06ff7653ef61713fa016f891
certbot-nginx==0.24.0 \
--hash=sha256:d44c419f72c2cc30de3b138a2cf92e0531696dcb048f287036e229dce2131c00 \
--hash=sha256:3283d1db057261f05537fa408baee20e0ab9e81c3d55cfba70afe3805cd6f941

View File

@@ -64,26 +64,37 @@ ConfigArgParse==0.12.0 \
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 \
--no-binary configobj
cryptography==2.2.2 \
--hash=sha256:3f3b65d5a16e6b52fba63dc860b62ca9832f51f1a2ae5083c78b6840275f12dd \
--hash=sha256:5251e7de0de66810833606439ca65c9b9e45da62196b0c88bfadf27740aac09f \
--hash=sha256:551a3abfe0c8c6833df4192a63371aa2ff43afd8f570ed345d31f251d78e7e04 \
--hash=sha256:5cb990056b7cadcca26813311187ad751ea644712022a3976443691168781b6f \
--hash=sha256:60bda7f12ecb828358be53095fc9c6edda7de8f1ef571f96c00b2363643fa3cd \
--hash=sha256:64b5c67acc9a7c83fbb4b69166f3105a0ab722d27934fac2cb26456718eec2ba \
--hash=sha256:6fef51ec447fe9f8351894024e94736862900d3a9aa2961528e602eb65c92bdb \
--hash=sha256:77d0ad229d47a6e0272d00f6bf8ac06ce14715a9fd02c9a97f5a2869aab3ccb2 \
--hash=sha256:808fe471b1a6b777f026f7dc7bd9a4959da4bfab64972f2bbe91e22527c1c037 \
--hash=sha256:9b62fb4d18529c84b961efd9187fecbb48e89aa1a0f9f4161c61b7fc42a101bd \
--hash=sha256:9e5bed45ec6b4f828866ac6a6bedf08388ffcfa68abe9e94b34bb40977aba531 \
--hash=sha256:9fc295bf69130a342e7a19a39d7bbeb15c0bcaabc7382ec33ef3b2b7d18d2f63 \
--hash=sha256:abd070b5849ed64e6d349199bef955ee0ad99aefbad792f0c587f8effa681a5e \
--hash=sha256:ba6a774749b6e510cffc2fb98535f717e0e5fd91c7c99a61d223293df79ab351 \
--hash=sha256:c332118647f084c983c6a3e1dba0f3bcb051f69d12baccac68db8d62d177eb8a \
--hash=sha256:d6f46e862ee36df81e6342c2177ba84e70f722d9dc9c6c394f9f1f434c4a5563 \
--hash=sha256:db6013746f73bf8edd9c3d1d3f94db635b9422f503db3fc5ef105233d4c011ab \
--hash=sha256:f57008eaff597c69cf692c3518f6d4800f0309253bb138b526a37fe9ef0c7471 \
--hash=sha256:f6c821ac253c19f2ad4c8691633ae1d1a17f120d5b01ea1d256d7b602bc59887
cryptography==2.0.2 \
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
--hash=sha256:da9291502cbc87dc0284a20c56876e4d2e68deac61cc43df4aec934e44ca97b1 \
--hash=sha256:0954f8813095f581669330e0a2d5e726c33ac7f450c1458fac58bab54595e516 \
--hash=sha256:d68b0cc40a8432ed3fc84876c519de704d6001800ec22b136e75ae841910c45b \
--hash=sha256:2f8ad9580ab4da645cfea52a91d2da99a49a1e76616d8be68441a986fad652b0 \
--hash=sha256:cc00b4511294f5f6b65c4e77a1a9c62f52490a63d2c120f3872176b40a82351e \
--hash=sha256:cf896020f6a9f095a547b3d672c8db1ef2ed71fca11250731fa1d4a4cb8b1590 \
--hash=sha256:e0fdb8322206fa02aa38f71519ff75dce2eb481b7e1110e2936795cb376bb6ee \
--hash=sha256:277538466657ca5d6637f80be100242f9831d75138b788d718edd3aab34621f8 \
--hash=sha256:2c77eb0560f54ce654ab82d6b2a64327a71ee969b29022bf9746ca311c9f5069 \
--hash=sha256:755a7853b679e79d0a799351c092a9b0271f95ff54c8dd8823d8b527a2926a86 \
--hash=sha256:77197a2d525e761cdd4c771180b4bd0d80703654c6385e4311cbbbe2beb56fa1 \
--hash=sha256:eb8bb79d0ab00c931c8333b745f06fec481a51c52d70acd4ee95d6093ba5c386 \
--hash=sha256:131f61de82ef28f3e20beb4bfc24f9692d28cecfd704e20e6c7f070f7793013a \
--hash=sha256:ac35435974b2e27cd4520f29c191d7da36f4189aa3264e52c4c6c6d089ab6142 \
--hash=sha256:04b6ea99daa2a8460728794213d76d45ad58ea247dc7e7ff148d7dd726e87863 \
--hash=sha256:2b9442f8b4c3d575f6cc3db0e856034e0f5a9d55ecd636f52d8c496795b26952 \
--hash=sha256:b3d3b3ecba1fe1bdb6f180770a137f877c8f07571f7b2934bb269475bcf0e5e8 \
--hash=sha256:670a58c0d75cb0e78e73dd003bd96d4440bbb1f2bc041dcf7b81767ca4fb0ce9 \
--hash=sha256:5af84d23bdb86b5e90aca263df1424b43f1748480bfcde3ac2a3cbe622612468 \
--hash=sha256:ba22e8eefabdd7aca37d0c0c00d2274000d2cebb5cce9e5a710cb55bf8797b31 \
--hash=sha256:b798b22fa7e92b439547323b8b719d217f1e1b7677585cfeeedf3b55c70bb7fb \
--hash=sha256:59cff28af8cce96cb7e94a459726e1d88f6f5fa75097f9dcbebd99118d64ea4c \
--hash=sha256:fe859e445abc9ba9e97950ddafb904e23234c4ecb76b0fae6c86e80592ce464a \
--hash=sha256:655f3c474067f1e277430f23cc0549f0b1dc99b82aec6e53f80b9b2db7f76f11 \
--hash=sha256:0ebc2be053c9a03a2f3e20a466e87bf12a51586b3c79bd2a22171b073a805346 \
--hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \
--hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \
--hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab
enum34==1.1.2 ; python_version < '3.4' \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501

Some files were not shown because too many files have changed in this diff Show More