Compare commits
27 Commits
test-pytho
...
test-pytho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8842c1e7eb | ||
|
|
142efc9458 | ||
|
|
0cee44f590 | ||
|
|
93d85920b9 | ||
|
|
681d36d1d6 | ||
|
|
71c7f09327 | ||
|
|
6d82061b42 | ||
|
|
a4a8b6e994 | ||
|
|
9defbc3aed | ||
|
|
f1461946b8 | ||
|
|
b7472fb337 | ||
|
|
937e27db28 | ||
|
|
e64137fb53 | ||
|
|
a0dc7dafda | ||
|
|
02975c84d9 | ||
|
|
5307600a03 | ||
|
|
b0ce7529aa | ||
|
|
5abec9fa3c | ||
|
|
8753959bb3 | ||
|
|
184b384b58 | ||
|
|
13b6b3f1b3 | ||
|
|
f4a1547481 | ||
|
|
164121fc15 | ||
|
|
349643c9b8 | ||
|
|
899b56514a | ||
|
|
fa3bc6d774 | ||
|
|
76f5f590ea |
@@ -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
|
||||
|
||||
|
||||
100
.travis.yml
100
.travis.yml
@@ -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:
|
||||
|
||||
86
CHANGELOG.md
86
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
include pytest.ini
|
||||
recursive-include docs *
|
||||
recursive-include examples *
|
||||
recursive-include acme/testdata *
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[pytest]
|
||||
norecursedirs = .* build dist CVS _darcs {arch} *.egg
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
acme[dev]==0.25.0
|
||||
-e .[dev]
|
||||
-e acme[dev]
|
||||
certbot[dev]==0.21.1
|
||||
|
||||
@@ -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',
|
||||
|
||||
47
certbot-auto
47
certbot-auto
@@ -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
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
acme[dev]==0.25.0
|
||||
-e acme[dev]
|
||||
certbot[dev]==0.21.1
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
server {
|
||||
server_name headers.com;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
acme[dev]==0.25.0
|
||||
-e acme[dev]
|
||||
-e .[dev]
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include certbot_postfix/testdata *
|
||||
recursive-include certbot_postfix/docs *
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
"""Certbot Postfix plugin."""
|
||||
|
||||
from certbot_postfix.installer import Installer
|
||||
@@ -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."""
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
""" Certbot Postfix Tests """
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
1
certbot-postfix/docs/.gitignore
vendored
1
certbot-postfix/docs/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/_build/
|
||||
@@ -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)
|
||||
@@ -1,8 +0,0 @@
|
||||
=================
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
api/**
|
||||
@@ -1,5 +0,0 @@
|
||||
:mod:`certbot_postfix.installer`
|
||||
--------------------------------------
|
||||
|
||||
.. automodule:: certbot_postfix.installer
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
:mod:`certbot_postfix.postconf`
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: certbot_postfix.postconf
|
||||
:members:
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -1,2 +0,0 @@
|
||||
acme[dev]==0.25.0
|
||||
certbot[dev]==0.23.0
|
||||
@@ -1,2 +0,0 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -36,7 +36,6 @@ class PluginEntryPoint(object):
|
||||
"certbot-dns-rfc2136",
|
||||
"certbot-dns-route53",
|
||||
"certbot-nginx",
|
||||
"certbot-postfix",
|
||||
]
|
||||
"""Distributions for which prefix will be omitted."""
|
||||
|
||||
|
||||
@@ -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]]
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 doesn’t, 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 we’re using Python 2-style annotations.
|
||||
|
||||
Zulip wrote a `great guide`_ to using mypy. It’s useful, but you don’t 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
|
||||
=========================
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -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-----
|
||||
|
||||
@@ -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
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user