diff --git a/CHANGELOG.md b/CHANGELOG.md index 1369b0907..1906858dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,110 @@ Certbot adheres to [Semantic Versioning](http://semver.org/). +## 0.22.2 - 2018-03-19 + +### Fixed + +* A type error introduced in 0.22.1 that would occur during challenge cleanup + when a Certbot plugin raises an exception while trying to complete the + challenge was fixed. + +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: + +* certbot + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/milestone/53?closed=1 + +## 0.22.1 - 2018-03-19 + +### Changed + +* The ACME server used with Certbot's --dry-run and --staging flags is now + Let's Encrypt's ACMEv2 staging server which allows people to also test ACMEv2 + features with these flags. + +### Fixed + +* The HTTP Content-Type header is now set to the correct value during + certificate revocation with new versions of the ACME protocol. +* When using Certbot with Let's Encrypt's ACMEv2 server, it would add a blank + line to the top of chain.pem and between the certificates in fullchain.pem + for each lineage. These blank lines have been removed. +* Resolved a bug that caused Certbot's --allow-subset-of-names flag not to + work. +* Fixed a regression in acme.client.Client that caused the class to not work + when it was initialized without a ClientNetwork which is done by some of the + other projects using our ACME library. + +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 + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/milestone/51?closed=1 + +## 0.22.0 - 2018-03-07 + +### Added + +* Support for obtaining wildcard certificates and a newer version of the ACME + protocol such as the one implemented by Let's Encrypt's upcoming ACMEv2 + endpoint was added to Certbot and its ACME library. Certbot still works with + older ACME versions and will automatically change the version of the protocol + used based on the version the ACME CA implements. +* The Apache and Nginx plugins are now able to automatically install a wildcard + certificate to multiple virtual hosts that you select from your server + configuration. +* The `certbot install` command now accepts the `--cert-name` flag for + selecting a certificate. +* `acme.client.BackwardsCompatibleClientV2` was added to Certbot's ACME library + which automatically handles most of the differences between new and old ACME + versions. `acme.client.ClientV2` is also available for people who only want + to support one version of the protocol or want to handle the differences + between versions themselves. +* certbot-auto now supports the flag --install-only which has the script + install Certbot and its dependencies and exit without invoking Certbot. +* Support for issuing a single certificate for a wildcard and base domain was + added to our Google Cloud DNS plugin. To do this, we now require your API + credentials have additional permissions, however, your credentials will + already have these permissions unless you defined a custom role with fewer + permissions than the standard DNS administrator role provided by Google. + These permissions are also only needed for the case described above so it + will continue to work for existing users. For more information about the + permissions changes, see the documentation in the plugin. + +### Changed + +* We have broken lockstep between our ACME library, Certbot, and its plugins. + This means that the different components do not need to be the same version + to work together like they did previously. This makes packaging easier + because not every piece of Certbot needs to be repackaged to ship a change to + a subset of its components. +* Support for Python 2.6 and Python 3.3 has been removed from ACME, Certbot, + Certbot's plugins, and certbot-auto. If you are using certbot-auto on a RHEL + 6 based system, it will walk you through the process of installing Certbot + with Python 3 and refuse to upgrade to a newer version of Certbot until you + have done so. +* Certbot's components now work with older versions of setuptools to simplify + packaging for EPEL 7. + +### Fixed + +* Issues caused by Certbot's Nginx plugin adding multiple ipv6only directives + has been resolved. +* A problem where Certbot's Apache plugin would add redundant include + directives for the TLS configuration managed by Certbot has been fixed. +* Certbot's webroot plugin now properly deletes any directories it creates. + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/milestone/48?closed=1 + ## 0.21.1 - 2018-01-25 ### Fixed diff --git a/acme/acme/client.py b/acme/acme/client.py index 9e2478afe..19615b087 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -259,11 +259,12 @@ class Client(ClientBase): """ # pylint: disable=too-many-arguments self.key = key - self.net = ClientNetwork(key, alg=alg, verify_ssl=verify_ssl) if net is None else net + if net is None: + net = ClientNetwork(key, alg=alg, verify_ssl=verify_ssl) if isinstance(directory, six.string_types): directory = messages.Directory.from_json( - self.net.get(directory).json()) + net.get(directory).json()) super(Client, self).__init__(directory=directory, net=net, acme_version=1) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 00b9e19dd..be08c2919 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -299,6 +299,16 @@ class ClientTest(ClientTestBase): directory=uri, key=KEY, alg=jose.RS256, net=self.net) self.net.get.assert_called_once_with(uri) + @mock.patch('acme.client.ClientNetwork') + def test_init_without_net(self, mock_net): + mock_net.return_value = mock.sentinel.net + alg = jose.RS256 + from acme.client import Client + self.client = Client( + directory=self.directory, key=KEY, alg=alg) + mock_net.called_once_with(KEY, alg=alg, verify_ssl=True) + self.assertEqual(self.client.net, mock.sentinel.net) + def test_register(self): # "Instance of 'Field' has no to_json/update member" bug: # pylint: disable=no-member diff --git a/acme/setup.py b/acme/setup.py index 071b56ab3..5660cf424 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Please update tox.ini when modifying dependency version requirements install_requires = [ diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 0ecbf88b3..224278a44 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -285,8 +285,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): chain_path=None, fullchain_path=None): """Deploys certificate to specified virtual host. - Currently tries to find the last directives to deploy the cert in - the VHost associated with the given domain. If it can't find the + Currently tries to find the last directives to deploy the certificate + in the VHost associated with the given domain. If it can't find the directives, it searches the "included" confs. The function verifies that it has located the three directives and finally modifies them to point to the correct destination. After the certificate is @@ -424,14 +424,20 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): path["chain_path"] = self.parser.find_dir( "SSLCertificateChainFile", None, vhost.path) - if not path["cert_path"] or not path["cert_key"]: - # Throw some can't find all of the directives error" + # Handle errors when certificate/key directives cannot be found + if not path["cert_path"]: logger.warning( - "Cannot find a cert or key directive in %s. " + "Cannot find an SSLCertificateFile directive in %s. " "VirtualHost was not modified", vhost.path) - # Presumably break here so that the virtualhost is not modified raise errors.PluginError( - "Unable to find cert and/or key directives") + "Unable to find an SSLCertificateFile directive") + elif not path["cert_key"]: + logger.warning( + "Cannot find an SSLCertificateKeyFile directive for " + "certificate in %s. VirtualHost was not modified", vhost.path) + raise errors.PluginError( + "Unable to find an SSLCertificateKeyFile directive for " + "certificate") logger.info("Deploying Certificate to VirtualHost %s", vhost.filep) @@ -2133,5 +2139,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) - - diff --git a/certbot-apache/certbot_apache/tests/configurator_test.py b/certbot-apache/certbot_apache/tests/configurator_test.py index 2d683d6ef..e33e16843 100644 --- a/certbot-apache/certbot_apache/tests/configurator_test.py +++ b/certbot-apache/certbot_apache/tests/configurator_test.py @@ -441,13 +441,37 @@ class MultipleVhostsTest(util.ApacheTest): self.vh_truth[1].path)) def test_deploy_cert_invalid_vhost(self): + """For test cases where the `ApacheConfigurator` class' `_deploy_cert` + method is called with an invalid vhost parameter. Currently this tests + that a PluginError is appropriately raised when important directives + are missing in an SSL module.""" self.config.parser.modules.add("ssl_module") - mock_find = mock.MagicMock() - mock_find.return_value = [] - self.config.parser.find_dir = mock_find + self.config.parser.modules.add("mod_ssl.c") + self.config.parser.modules.add("socache_shmcb_module") + + def side_effect(*args): + """Mocks case where an SSLCertificateFile directive can be found + but an SSLCertificateKeyFile directive is missing.""" + if "SSLCertificateFile" in args: + return ["example/cert.pem"] + else: + return [] + + mock_find_dir = mock.MagicMock(return_value=[]) + mock_find_dir.side_effect = side_effect + + self.config.parser.find_dir = mock_find_dir # Get the default 443 vhost self.config.assoc["random.demo"] = self.vh_truth[1] + + self.assertRaises( + errors.PluginError, self.config.deploy_cert, "random.demo", + "example/cert.pem", "example/key.pem", "example/cert_chain.pem") + + # Remove side_effect to mock case where both SSLCertificateFile + # and SSLCertificateKeyFile directives are missing + self.config.parser.find_dir.side_effect = None self.assertRaises( errors.PluginError, self.config.deploy_cert, "random.demo", "example/cert.pem", "example/key.pem", "example/cert_chain.pem") diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 7608c0647..f00b6d95d 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-auto b/certbot-auto index d3a5c23e5..8c9745a6f 100755 --- a/certbot-auto +++ b/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.21.1" +LE_AUTO_VERSION="0.22.2" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -47,6 +47,7 @@ Help for certbot itself cannot be provided until it is installed. --no-bootstrap do not install OS dependencies --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit + --install-only install certbot, upgrade if needed, and exit -v, --verbose provide more output -q, --quiet provide only update/error output; implies --non-interactive @@ -60,6 +61,8 @@ for arg in "$@" ; do DEBUG=1;; --os-packages-only) OS_PACKAGES_ONLY=1;; + --install-only) + INSTALL_ONLY=1;; --no-self-upgrade) # Do not upgrade this script (also prevents client upgrades, because each # copy of the script pins a hash of the python client) @@ -246,7 +249,7 @@ DeprecationBootstrap() { fi } -MIN_PYTHON_VERSION="2.6" +MIN_PYTHON_VERSION="2.7" MIN_PYVER=$(echo "$MIN_PYTHON_VERSION" | sed 's/\.//') # Sets LE_PYTHON to Python version string and PYVER to the first two # digits of the python version @@ -1196,24 +1199,24 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.21.1 \ - --hash=sha256:08f026078807fbcfd7bfab44c4d827ee287738fefcc86fbe1493ce752d2fdccb \ - --hash=sha256:e6c8e9b0b5e38834330831d5a91e1c08accdb9b4923855d14d524e7327e6c4ea -acme==0.21.1 \ - --hash=sha256:4b2b5ef80c755dfa30eb5c67ab4b4e66e7f205ad922b43170502c5f8d8ef1242 \ - --hash=sha256:296e8abf4f5a69af1a892416faceea90e15f39e2920bf87beeaad1d6ce70a60b -certbot-apache==0.21.1 \ - --hash=sha256:faa4af1033564a0e676d16940775593fb849527b494a15f6a816ad0ed4fa273c \ - --hash=sha256:0bce4419d4fdabbdda2223cff8db6794c5717632fb9511b00498ec00982a3fa5 -certbot-nginx==0.21.1 \ - --hash=sha256:3fad3b4722544558ce03132f853e18da5e516013086aaa40f1036aa6667c70a9 \ - --hash=sha256:55a32afe0950ff49d3118f93035463a46c85c2f399d261123f5fe973afdd4f64 +certbot==0.22.2 \ + --hash=sha256:c8c63bdf0fed6258bdbc892454314ec37bcd1c35a7f62524a083d93ccdfc420d \ + --hash=sha256:e6e3639293e78397f31f7d99e3c63aff82d91e2b0d50d146ee3c77f830464bef +acme==0.22.2 \ + --hash=sha256:59a55244612ee305d2caa6bb4cddd400fb60ec841bf011ed29a2899832a682c2 \ + --hash=sha256:0ecd0ea369f53d5bc744d6e72717f9af2e1ceb558d109dbd433148851027adb4 +certbot-apache==0.22.2 \ + --hash=sha256:b5340d4b9190358fde8eb6a5be0def37e32014b5142ee79ef5d2319ccbbde754 \ + --hash=sha256:3cd26912bb5732d917ddf7aad2fe870090d4ece9a408b2c2de8e9723ec99c759 +certbot-nginx==0.22.2 \ + --hash=sha256:91feef0d879496835d355e82841f92e5ecb5abbf6f23ea0ee5bbb8f5a92b278a \ + --hash=sha256:b10bf04c1a20cf878d5e0d1877deb0e0780bc31b0ffda08ce7199bbc39d0753b UNLIKELY_EOF # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py" #!/usr/bin/env python -"""A small script that can act as a trust root for installing pip 8 +"""A small script that can act as a trust root for installing pip >=8 Embed this in your project, and your VCS checkout is all you have to trust. In a post-peep era, this lets you claw your way to a hash-checking version of pip, @@ -1237,6 +1240,7 @@ anything goes wrong, it will exit with a non-zero status code. from __future__ import print_function from distutils.version import StrictVersion from hashlib import sha256 +from os import environ from os.path import join from pipes import quote from shutil import rmtree @@ -1270,14 +1274,14 @@ except ImportError: from urllib.parse import urlparse # 3.4 -__version__ = 1, 3, 0 +__version__ = 1, 5, 1 PIP_VERSION = '9.0.1' +DEFAULT_INDEX_BASE = 'https://pypi.python.org' # wheel has a conditional dependency on argparse: maybe_argparse = ( - [('https://pypi.python.org/packages/18/dd/' - 'e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/' + [('18/dd/e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/' 'argparse-1.4.0.tar.gz', '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')] if version_info < (2, 7, 0) else []) @@ -1285,18 +1289,14 @@ maybe_argparse = ( PACKAGES = maybe_argparse + [ # Pip has no dependencies, as it vendors everything: - ('https://pypi.python.org/packages/11/b6/' - 'abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/' - 'pip-{0}.tar.gz' - .format(PIP_VERSION), + ('11/b6/abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/' + 'pip-{0}.tar.gz'.format(PIP_VERSION), '09f243e1a7b461f654c26a725fa373211bb7ff17a9300058b205c61658ca940d'), # This version of setuptools has only optional dependencies: - ('https://pypi.python.org/packages/69/65/' - '4c544cde88d4d876cdf5cbc5f3f15d02646477756d89547e9a7ecd6afa76/' - 'setuptools-20.2.2.tar.gz', - '24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'), - ('https://pypi.python.org/packages/c9/1d/' - 'bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/' + ('59/88/2f3990916931a5de6fa9706d6d75eb32ee8b78627bb2abaab7ed9e6d0622/' + 'setuptools-29.0.1.tar.gz', + 'b539118819a4857378398891fa5366e090690e46b3e41421a1e07d6e9fd8feb0'), + ('c9/1d/bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/' 'wheel-0.29.0.tar.gz', '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648') ] @@ -1317,12 +1317,13 @@ def hashed_download(url, temp, digest): # >=2.7.9 verifies HTTPS certs itself, and, in any case, the cert # authenticity has only privacy (not arbitrary code execution) # implications, since we're checking hashes. - def opener(): + def opener(using_https=True): opener = build_opener(HTTPSHandler()) - # Strip out HTTPHandler to prevent MITM spoof: - for handler in opener.handlers: - if isinstance(handler, HTTPHandler): - opener.handlers.remove(handler) + if using_https: + # Strip out HTTPHandler to prevent MITM spoof: + for handler in opener.handlers: + if isinstance(handler, HTTPHandler): + opener.handlers.remove(handler) return opener def read_chunks(response, chunk_size): @@ -1332,8 +1333,9 @@ def hashed_download(url, temp, digest): break yield chunk - response = opener().open(url) - path = join(temp, urlparse(url).path.split('/')[-1]) + parsed_url = urlparse(url) + response = opener(using_https=parsed_url.scheme == 'https').open(url) + path = join(temp, parsed_url.path.split('/')[-1]) actual_hash = sha256() with open(path, 'wb') as file: for chunk in read_chunks(response, 4096): @@ -1346,6 +1348,24 @@ def hashed_download(url, temp, digest): return path +def get_index_base(): + """Return the URL to the dir containing the "packages" folder. + + Try to wring something out of PIP_INDEX_URL, if set. Hack "/simple" off the + end if it's there; that is likely to give us the right dir. + + """ + env_var = environ.get('PIP_INDEX_URL', '').rstrip('/') + if env_var: + SIMPLE = '/simple' + if env_var.endswith(SIMPLE): + return env_var[:-len(SIMPLE)] + else: + return env_var + else: + return DEFAULT_INDEX_BASE + + def main(): pip_version = StrictVersion(check_output(['pip', '--version']) .decode('utf-8').split()[1]) @@ -1353,11 +1373,13 @@ def main(): if pip_version >= min_pip_version: return 0 has_pip_cache = pip_version >= StrictVersion('6.0') - + index_base = get_index_base() temp = mkdtemp(prefix='pipstrap-') try: - downloads = [hashed_download(url, temp, digest) - for url, digest in PACKAGES] + downloads = [hashed_download(index_base + '/packages/' + path, + temp, + digest) + for path, digest in PACKAGES] check_output('pip install --no-index --no-deps -U ' + # Disable cache since we're not using it and it otherwise # sometimes throws permission warnings: @@ -1428,6 +1450,12 @@ UNLIKELY_EOF say "Installation succeeded." fi + + if [ "$INSTALL_ONLY" = 1 ]; then + say "Certbot is installed." + exit 0 + fi + "$VENV_BIN/letsencrypt" "$@" else diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 71a0ba574..2c6c917b3 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -10,6 +10,8 @@ import sys import OpenSSL +from six.moves import xrange # pylint: disable=import-error,redefined-builtin + from acme import challenges from acme import crypto_util from acme import messages diff --git a/certbot-compatibility-test/certbot_compatibility_test/validator.py b/certbot-compatibility-test/certbot_compatibility_test/validator.py index 0fd6efab5..791fe0da2 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/validator.py +++ b/certbot-compatibility-test/certbot_compatibility_test/validator.py @@ -5,6 +5,7 @@ import requests import zope.interface import six +from six.moves import xrange # pylint: disable=import-error,redefined-builtin from acme import crypto_util from acme import errors as acme_errors diff --git a/certbot-compatibility-test/nginx/roundtrip.py b/certbot-compatibility-test/nginx/roundtrip.py index 852221df5..85d283c78 100644 --- a/certbot-compatibility-test/nginx/roundtrip.py +++ b/certbot-compatibility-test/nginx/roundtrip.py @@ -8,7 +8,7 @@ from certbot_nginx import nginxparser def roundtrip(stuff): success = True for t in stuff: - print t + print(t) if not os.path.isfile(t): continue with open(t, "r") as f: diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 861921ef7..17abe65ec 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/docs/index.rst b/certbot-dns-cloudflare/docs/index.rst index e75106a01..f2d59baea 100644 --- a/certbot-dns-cloudflare/docs/index.rst +++ b/certbot-dns-cloudflare/docs/index.rst @@ -10,14 +10,14 @@ Welcome to certbot-dns-cloudflare's documentation! :maxdepth: 2 :caption: Contents: +.. automodule:: certbot_dns_cloudflare + :members: + .. toctree:: :maxdepth: 1 api -.. automodule:: certbot_dns_cloudflare - :members: - Indices and tables ================== diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 4ed8e796d..956e37f79 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-cloudxns/docs/index.rst b/certbot-dns-cloudxns/docs/index.rst index 41ea250cd..83c6ca18d 100644 --- a/certbot-dns-cloudxns/docs/index.rst +++ b/certbot-dns-cloudxns/docs/index.rst @@ -10,14 +10,14 @@ Welcome to certbot-dns-cloudxns's documentation! :maxdepth: 2 :caption: Contents: +.. automodule:: certbot_dns_cloudxns + :members: + .. toctree:: :maxdepth: 1 api -.. automodule:: certbot_dns_cloudxns - :members: - Indices and tables diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 7f973709c..3493638a0 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -4,14 +4,14 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. install_requires = [ 'acme>=0.21.1', 'certbot>=0.21.1', - 'dns-lexicon', + 'dns-lexicon>=2.2.1', # Support for >1 TXT record per name 'mock', 'setuptools', 'zope.interface', diff --git a/certbot-dns-digitalocean/docs/index.rst b/certbot-dns-digitalocean/docs/index.rst index 9f66382ee..5e30f2d2c 100644 --- a/certbot-dns-digitalocean/docs/index.rst +++ b/certbot-dns-digitalocean/docs/index.rst @@ -10,14 +10,14 @@ Welcome to certbot-dns-digitalocean's documentation! :maxdepth: 2 :caption: Contents: +.. automodule:: certbot_dns_digitalocean + :members: + .. toctree:: :maxdepth: 1 api -.. automodule:: certbot_dns_digitalocean - :members: - Indices and tables diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index 0ce91e64e..f136c7161 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-dnsimple/docs/index.rst b/certbot-dns-dnsimple/docs/index.rst index 4ff1e59eb..a565ba919 100644 --- a/certbot-dns-dnsimple/docs/index.rst +++ b/certbot-dns-dnsimple/docs/index.rst @@ -10,14 +10,14 @@ Welcome to certbot-dns-dnsimple's documentation! :maxdepth: 2 :caption: Contents: +.. automodule:: certbot_dns_dnsimple + :members: + .. toctree:: :maxdepth: 1 api -.. automodule:: certbot_dns_dnsimple - :members: - Indices and tables diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index d12b26d83..f2887d371 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,14 +4,14 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. install_requires = [ 'acme>=0.21.1', 'certbot>=0.21.1', - 'dns-lexicon', + 'dns-lexicon>=2.2.1', # Support for >1 TXT record per name 'mock', 'setuptools', 'zope.interface', diff --git a/certbot-dns-dnsmadeeasy/docs/index.rst b/certbot-dns-dnsmadeeasy/docs/index.rst index 2e9aef36b..dddf0e745 100644 --- a/certbot-dns-dnsmadeeasy/docs/index.rst +++ b/certbot-dns-dnsmadeeasy/docs/index.rst @@ -10,14 +10,14 @@ Welcome to certbot-dns-dnsmadeeasy's documentation! :maxdepth: 2 :caption: Contents: +.. automodule:: certbot_dns_dnsmadeeasy + :members: + .. toctree:: :maxdepth: 1 api -.. automodule:: certbot_dns_dnsmadeeasy - :members: - Indices and tables diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index 856eaba0f..a3ee12cf0 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -4,14 +4,14 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. install_requires = [ 'acme>=0.21.1', 'certbot>=0.21.1', - 'dns-lexicon', + 'dns-lexicon>=2.2.1', # Support for >1 TXT record per name 'mock', 'setuptools', 'zope.interface', diff --git a/certbot-dns-google/certbot_dns_google/dns_google.py b/certbot-dns-google/certbot_dns_google/dns_google.py index e2088b357..c204cb0ca 100644 --- a/certbot-dns-google/certbot_dns_google/dns_google.py +++ b/certbot-dns-google/certbot_dns_google/dns_google.py @@ -81,7 +81,7 @@ class _GoogleClient(object): Encapsulates all communication with the Google Cloud DNS API. """ - def __init__(self, account_json=None): + def __init__(self, account_json=None, dns_api=None): scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite'] if account_json is not None: @@ -92,7 +92,12 @@ class _GoogleClient(object): credentials = None self.project_id = self.get_project_id() - self.dns = discovery.build('dns', 'v1', credentials=credentials, cache_discovery=False) + if not dns_api: + self.dns = discovery.build('dns', 'v1', + credentials=credentials, + cache_discovery=False) + else: + self.dns = dns_api def add_txt_record(self, domain, record_name, record_content, record_ttl): """ diff --git a/certbot-dns-google/certbot_dns_google/dns_google_test.py b/certbot-dns-google/certbot_dns_google/dns_google_test.py index 72b8be8af..b6f6e08b6 100644 --- a/certbot-dns-google/certbot_dns_google/dns_google_test.py +++ b/certbot-dns-google/certbot_dns_google/dns_google_test.py @@ -4,7 +4,9 @@ import os import unittest import mock +from googleapiclient import discovery from googleapiclient.errors import Error +from googleapiclient.http import HttpMock from httplib2 import ServerNotFoundError from certbot import errors @@ -68,7 +70,13 @@ class GoogleClientTest(unittest.TestCase): def _setUp_client_with_mock(self, zone_request_side_effect): from certbot_dns_google.dns_google import _GoogleClient - client = _GoogleClient(ACCOUNT_JSON_PATH) + pwd = os.path.dirname(__file__) + rel_path = 'testdata/discovery.json' + discovery_file = os.path.join(pwd, rel_path) + http_mock = HttpMock(discovery_file, {'status': '200'}) + dns_api = discovery.build('dns', 'v1', http=http_mock) + + client = _GoogleClient(ACCOUNT_JSON_PATH, dns_api) # Setup mock_mz = mock.MagicMock() diff --git a/certbot-dns-google/certbot_dns_google/testdata/discovery.json b/certbot-dns-google/certbot_dns_google/testdata/discovery.json new file mode 100644 index 000000000..79a406645 --- /dev/null +++ b/certbot-dns-google/certbot_dns_google/testdata/discovery.json @@ -0,0 +1,1401 @@ +{ + "kind": "discovery#restDescription", + "etag": "\"-iA1DTNe4s-I6JZXPt1t1Ypy8IU/gSzgHqX4Zwypnde2YApimTf_qmE\"", + "discoveryVersion": "v1", + "id": "dns:v1", + "name": "dns", + "version": "v1", + "revision": "20180314", + "title": "Google Cloud DNS API", + "description": "Configures and serves authoritative DNS records.", + "ownerDomain": "google.com", + "ownerName": "Google", + "icons": { + "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", + "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" + }, + "documentationLink": "https://developers.google.com/cloud-dns", + "protocol": "rest", + "baseUrl": "https://www.googleapis.com/dns/v1/projects/", + "basePath": "/dns/v1/projects/", + "rootUrl": "https://www.googleapis.com/", + "servicePath": "dns/v1/projects/", + "batchPath": "batch/dns/v1", + "parameters": { + "alt": { + "type": "string", + "description": "Data format for the response.", + "default": "json", + "enum": [ + "json" + ], + "enumDescriptions": [ + "Responses with Content-Type of application/json" + ], + "location": "query" + }, + "fields": { + "type": "string", + "description": "Selector specifying which fields to include in a partial response.", + "location": "query" + }, + "key": { + "type": "string", + "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", + "location": "query" + }, + "oauth_token": { + "type": "string", + "description": "OAuth 2.0 token for the current user.", + "location": "query" + }, + "prettyPrint": { + "type": "boolean", + "description": "Returns response with indentations and line breaks.", + "default": "true", + "location": "query" + }, + "quotaUser": { + "type": "string", + "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.", + "location": "query" + }, + "userIp": { + "type": "string", + "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.", + "location": "query" + } + }, + "auth": { + "oauth2": { + "scopes": { + "https://www.googleapis.com/auth/cloud-platform": { + "description": "View and manage your data across Google Cloud Platform services" + }, + "https://www.googleapis.com/auth/cloud-platform.read-only": { + "description": "View your data across Google Cloud Platform services" + }, + "https://www.googleapis.com/auth/ndev.clouddns.readonly": { + "description": "View your DNS records hosted by Google Cloud DNS" + }, + "https://www.googleapis.com/auth/ndev.clouddns.readwrite": { + "description": "View and manage your DNS records hosted by Google Cloud DNS" + } + } + } + }, + "schemas": { + "Change": { + "id": "Change", + "type": "object", + "description": "An atomic update to a collection of ResourceRecordSets.", + "properties": { + "additions": { + "type": "array", + "description": "Which ResourceRecordSets to add?", + "items": { + "$ref": "ResourceRecordSet" + } + }, + "deletions": { + "type": "array", + "description": "Which ResourceRecordSets to remove? Must match existing data exactly.", + "items": { + "$ref": "ResourceRecordSet" + } + }, + "id": { + "type": "string", + "description": "Unique identifier for the resource; defined by the server (output only)." + }, + "isServing": { + "type": "boolean", + "description": "If the DNS queries for the zone will be served." + }, + "kind": { + "type": "string", + "description": "Identifies what kind of resource this is. Value: the fixed string \"dns#change\".", + "default": "dns#change" + }, + "startTime": { + "type": "string", + "description": "The time that this operation was started by the server (output only). This is in RFC3339 text format." + }, + "status": { + "type": "string", + "description": "Status of the operation (output only).", + "enum": [ + "done", + "pending" + ], + "enumDescriptions": [ + "", + "" + ] + } + } + }, + "ChangesListResponse": { + "id": "ChangesListResponse", + "type": "object", + "description": "The response to a request to enumerate Changes to a ResourceRecordSets collection.", + "properties": { + "changes": { + "type": "array", + "description": "The requested changes.", + "items": { + "$ref": "Change" + } + }, + "header": { + "$ref": "ResponseHeader" + }, + "kind": { + "type": "string", + "description": "Type of resource.", + "default": "dns#changesListResponse" + }, + "nextPageToken": { + "type": "string", + "description": "The presence of this field indicates that there exist more results following your last page of results in pagination order. To fetch them, make another list request using this value as your pagination token.\n\nIn this way you can retrieve the complete contents of even very large collections one page at a time. However, if the contents of the collection change between the first and last paginated list request, the set of all elements returned will be an inconsistent view of the collection. There is no way to retrieve a \"snapshot\" of collections larger than the maximum page size." + } + } + }, + "DnsKey": { + "id": "DnsKey", + "type": "object", + "description": "A DNSSEC key pair.", + "properties": { + "algorithm": { + "type": "string", + "description": "String mnemonic specifying the DNSSEC algorithm of this key. Immutable after creation time.", + "enum": [ + "ecdsap256sha256", + "ecdsap384sha384", + "rsasha1", + "rsasha256", + "rsasha512" + ], + "enumDescriptions": [ + "", + "", + "", + "", + "" + ] + }, + "creationTime": { + "type": "string", + "description": "The time that this resource was created in the control plane. This is in RFC3339 text format. Output only." + }, + "description": { + "type": "string", + "description": "A mutable string of at most 1024 characters associated with this resource for the user's convenience. Has no effect on the resource's function." + }, + "digests": { + "type": "array", + "description": "Cryptographic hashes of the DNSKEY resource record associated with this DnsKey. These digests are needed to construct a DS record that points at this DNS key. Output only.", + "items": { + "$ref": "DnsKeyDigest" + } + }, + "id": { + "type": "string", + "description": "Unique identifier for the resource; defined by the server (output only)." + }, + "isActive": { + "type": "boolean", + "description": "Active keys will be used to sign subsequent changes to the ManagedZone. Inactive keys will still be present as DNSKEY Resource Records for the use of resolvers validating existing signatures." + }, + "keyLength": { + "type": "integer", + "description": "Length of the key in bits. Specified at creation time then immutable.", + "format": "uint32" + }, + "keyTag": { + "type": "integer", + "description": "The key tag is a non-cryptographic hash of the a DNSKEY resource record associated with this DnsKey. The key tag can be used to identify a DNSKEY more quickly (but it is not a unique identifier). In particular, the key tag is used in a parent zone's DS record to point at the DNSKEY in this child ManagedZone. The key tag is a number in the range [0, 65535] and the algorithm to calculate it is specified in RFC4034 Appendix B. Output only.", + "format": "int32" + }, + "kind": { + "type": "string", + "description": "Identifies what kind of resource this is. Value: the fixed string \"dns#dnsKey\".", + "default": "dns#dnsKey" + }, + "publicKey": { + "type": "string", + "description": "Base64 encoded public half of this key. Output only." + }, + "type": { + "type": "string", + "description": "One of \"KEY_SIGNING\" or \"ZONE_SIGNING\". Keys of type KEY_SIGNING have the Secure Entry Point flag set and, when active, will be used to sign only resource record sets of type DNSKEY. Otherwise, the Secure Entry Point flag will be cleared and this key will be used to sign only resource record sets of other types. Immutable after creation time.", + "enum": [ + "keySigning", + "zoneSigning" + ], + "enumDescriptions": [ + "", + "" + ] + } + } + }, + "DnsKeyDigest": { + "id": "DnsKeyDigest", + "type": "object", + "properties": { + "digest": { + "type": "string", + "description": "The base-16 encoded bytes of this digest. Suitable for use in a DS resource record." + }, + "type": { + "type": "string", + "description": "Specifies the algorithm used to calculate this digest.", + "enum": [ + "sha1", + "sha256", + "sha384" + ], + "enumDescriptions": [ + "", + "", + "" + ] + } + } + }, + "DnsKeySpec": { + "id": "DnsKeySpec", + "type": "object", + "description": "Parameters for DnsKey key generation. Used for generating initial keys for a new ManagedZone and as default when adding a new DnsKey.", + "properties": { + "algorithm": { + "type": "string", + "description": "String mnemonic specifying the DNSSEC algorithm of this key.", + "enum": [ + "ecdsap256sha256", + "ecdsap384sha384", + "rsasha1", + "rsasha256", + "rsasha512" + ], + "enumDescriptions": [ + "", + "", + "", + "", + "" + ] + }, + "keyLength": { + "type": "integer", + "description": "Length of the keys in bits.", + "format": "uint32" + }, + "keyType": { + "type": "string", + "description": "One of \"KEY_SIGNING\" or \"ZONE_SIGNING\". Keys of type KEY_SIGNING have the Secure Entry Point flag set and, when active, will be used to sign only resource record sets of type DNSKEY. Otherwise, the Secure Entry Point flag will be cleared and this key will be used to sign only resource record sets of other types.", + "enum": [ + "keySigning", + "zoneSigning" + ], + "enumDescriptions": [ + "", + "" + ] + }, + "kind": { + "type": "string", + "description": "Identifies what kind of resource this is. Value: the fixed string \"dns#dnsKeySpec\".", + "default": "dns#dnsKeySpec" + } + } + }, + "DnsKeysListResponse": { + "id": "DnsKeysListResponse", + "type": "object", + "description": "The response to a request to enumerate DnsKeys in a ManagedZone.", + "properties": { + "dnsKeys": { + "type": "array", + "description": "The requested resources.", + "items": { + "$ref": "DnsKey" + } + }, + "header": { + "$ref": "ResponseHeader" + }, + "kind": { + "type": "string", + "description": "Type of resource.", + "default": "dns#dnsKeysListResponse" + }, + "nextPageToken": { + "type": "string", + "description": "The presence of this field indicates that there exist more results following your last page of results in pagination order. To fetch them, make another list request using this value as your pagination token.\n\nIn this way you can retrieve the complete contents of even very large collections one page at a time. However, if the contents of the collection change between the first and last paginated list request, the set of all elements returned will be an inconsistent view of the collection. There is no way to retrieve a \"snapshot\" of collections larger than the maximum page size." + } + } + }, + "ManagedZone": { + "id": "ManagedZone", + "type": "object", + "description": "A zone is a subtree of the DNS namespace under one administrative responsibility. A ManagedZone is a resource that represents a DNS zone hosted by the Cloud DNS service.", + "properties": { + "creationTime": { + "type": "string", + "description": "The time that this resource was created on the server. This is in RFC3339 text format. Output only." + }, + "description": { + "type": "string", + "description": "A mutable string of at most 1024 characters associated with this resource for the user's convenience. Has no effect on the managed zone's function." + }, + "dnsName": { + "type": "string", + "description": "The DNS name of this managed zone, for instance \"example.com.\"." + }, + "dnssecConfig": { + "$ref": "ManagedZoneDnsSecConfig", + "description": "DNSSEC configuration." + }, + "id": { + "type": "string", + "description": "Unique identifier for the resource; defined by the server (output only)", + "format": "uint64" + }, + "kind": { + "type": "string", + "description": "Identifies what kind of resource this is. Value: the fixed string \"dns#managedZone\".", + "default": "dns#managedZone" + }, + "labels": { + "type": "object", + "description": "User labels.", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string", + "description": "User assigned name for this resource. Must be unique within the project. The name must be 1-63 characters long, must begin with a letter, end with a letter or digit, and only contain lowercase letters, digits or dashes." + }, + "nameServerSet": { + "type": "string", + "description": "Optionally specifies the NameServerSet for this ManagedZone. A NameServerSet is a set of DNS name servers that all host the same ManagedZones. Most users will leave this field unset." + }, + "nameServers": { + "type": "array", + "description": "Delegate your managed_zone to these virtual name servers; defined by the server (output only)", + "items": { + "type": "string" + } + } + } + }, + "ManagedZoneDnsSecConfig": { + "id": "ManagedZoneDnsSecConfig", + "type": "object", + "properties": { + "defaultKeySpecs": { + "type": "array", + "description": "Specifies parameters that will be used for generating initial DnsKeys for this ManagedZone. Output only while state is not OFF.", + "items": { + "$ref": "DnsKeySpec" + } + }, + "kind": { + "type": "string", + "description": "Identifies what kind of resource this is. Value: the fixed string \"dns#managedZoneDnsSecConfig\".", + "default": "dns#managedZoneDnsSecConfig" + }, + "nonExistence": { + "type": "string", + "description": "Specifies the mechanism used to provide authenticated denial-of-existence responses. Output only while state is not OFF.", + "enum": [ + "nsec", + "nsec3" + ], + "enumDescriptions": [ + "", + "" + ] + }, + "state": { + "type": "string", + "description": "Specifies whether DNSSEC is enabled, and what mode it is in.", + "enum": [ + "off", + "on", + "transfer" + ], + "enumDescriptions": [ + "", + "", + "" + ] + } + } + }, + "ManagedZoneOperationsListResponse": { + "id": "ManagedZoneOperationsListResponse", + "type": "object", + "properties": { + "header": { + "$ref": "ResponseHeader" + }, + "kind": { + "type": "string", + "description": "Type of resource.", + "default": "dns#managedZoneOperationsListResponse" + }, + "nextPageToken": { + "type": "string", + "description": "The presence of this field indicates that there exist more results following your last page of results in pagination order. To fetch them, make another list request using this value as your page token.\n\nIn this way you can retrieve the complete contents of even very large collections one page at a time. However, if the contents of the collection change between the first and last paginated list request, the set of all elements returned will be an inconsistent view of the collection. There is no way to retrieve a consistent snapshot of a collection larger than the maximum page size." + }, + "operations": { + "type": "array", + "description": "The operation resources.", + "items": { + "$ref": "Operation" + } + } + } + }, + "ManagedZonesListResponse": { + "id": "ManagedZonesListResponse", + "type": "object", + "properties": { + "header": { + "$ref": "ResponseHeader" + }, + "kind": { + "type": "string", + "description": "Type of resource.", + "default": "dns#managedZonesListResponse" + }, + "managedZones": { + "type": "array", + "description": "The managed zone resources.", + "items": { + "$ref": "ManagedZone" + } + }, + "nextPageToken": { + "type": "string", + "description": "The presence of this field indicates that there exist more results following your last page of results in pagination order. To fetch them, make another list request using this value as your page token.\n\nIn this way you can retrieve the complete contents of even very large collections one page at a time. However, if the contents of the collection change between the first and last paginated list request, the set of all elements returned will be an inconsistent view of the collection. There is no way to retrieve a consistent snapshot of a collection larger than the maximum page size." + } + } + }, + "Operation": { + "id": "Operation", + "type": "object", + "description": "An operation represents a successful mutation performed on a Cloud DNS resource. Operations provide: - An audit log of server resource mutations. - A way to recover/retry API calls in the case where the response is never received by the caller. Use the caller specified client_operation_id.", + "properties": { + "dnsKeyContext": { + "$ref": "OperationDnsKeyContext", + "description": "Only populated if the operation targeted a DnsKey (output only)." + }, + "id": { + "type": "string", + "description": "Unique identifier for the resource. This is the client_operation_id if the client specified it when the mutation was initiated, otherwise, it is generated by the server. The name must be 1-63 characters long and match the regular expression [-a-z0-9]? (output only)" + }, + "kind": { + "type": "string", + "description": "Identifies what kind of resource this is. Value: the fixed string \"dns#operation\".", + "default": "dns#operation" + }, + "startTime": { + "type": "string", + "description": "The time that this operation was started by the server. This is in RFC3339 text format (output only)." + }, + "status": { + "type": "string", + "description": "Status of the operation. Can be one of the following: \"PENDING\" or \"DONE\" (output only).", + "enum": [ + "done", + "pending" + ], + "enumDescriptions": [ + "", + "" + ] + }, + "type": { + "type": "string", + "description": "Type of the operation. Operations include insert, update, and delete (output only)." + }, + "user": { + "type": "string", + "description": "User who requested the operation, for example: user@example.com. cloud-dns-system for operations automatically done by the system. (output only)" + }, + "zoneContext": { + "$ref": "OperationManagedZoneContext", + "description": "Only populated if the operation targeted a ManagedZone (output only)." + } + } + }, + "OperationDnsKeyContext": { + "id": "OperationDnsKeyContext", + "type": "object", + "properties": { + "newValue": { + "$ref": "DnsKey", + "description": "The post-operation DnsKey resource." + }, + "oldValue": { + "$ref": "DnsKey", + "description": "The pre-operation DnsKey resource." + } + } + }, + "OperationManagedZoneContext": { + "id": "OperationManagedZoneContext", + "type": "object", + "properties": { + "newValue": { + "$ref": "ManagedZone", + "description": "The post-operation ManagedZone resource." + }, + "oldValue": { + "$ref": "ManagedZone", + "description": "The pre-operation ManagedZone resource." + } + } + }, + "Project": { + "id": "Project", + "type": "object", + "description": "A project resource. The project is a top level container for resources including Cloud DNS ManagedZones. Projects can be created only in the APIs console.", + "properties": { + "id": { + "type": "string", + "description": "User assigned unique identifier for the resource (output only)." + }, + "kind": { + "type": "string", + "description": "Identifies what kind of resource this is. Value: the fixed string \"dns#project\".", + "default": "dns#project" + }, + "number": { + "type": "string", + "description": "Unique numeric identifier for the resource; defined by the server (output only).", + "format": "uint64" + }, + "quota": { + "$ref": "Quota", + "description": "Quotas assigned to this project (output only)." + } + } + }, + "Quota": { + "id": "Quota", + "type": "object", + "description": "Limits associated with a Project.", + "properties": { + "dnsKeysPerManagedZone": { + "type": "integer", + "description": "Maximum allowed number of DnsKeys per ManagedZone.", + "format": "int32" + }, + "kind": { + "type": "string", + "description": "Identifies what kind of resource this is. Value: the fixed string \"dns#quota\".", + "default": "dns#quota" + }, + "managedZones": { + "type": "integer", + "description": "Maximum allowed number of managed zones in the project.", + "format": "int32" + }, + "resourceRecordsPerRrset": { + "type": "integer", + "description": "Maximum allowed number of ResourceRecords per ResourceRecordSet.", + "format": "int32" + }, + "rrsetAdditionsPerChange": { + "type": "integer", + "description": "Maximum allowed number of ResourceRecordSets to add per ChangesCreateRequest.", + "format": "int32" + }, + "rrsetDeletionsPerChange": { + "type": "integer", + "description": "Maximum allowed number of ResourceRecordSets to delete per ChangesCreateRequest.", + "format": "int32" + }, + "rrsetsPerManagedZone": { + "type": "integer", + "description": "Maximum allowed number of ResourceRecordSets per zone in the project.", + "format": "int32" + }, + "totalRrdataSizePerChange": { + "type": "integer", + "description": "Maximum allowed size for total rrdata in one ChangesCreateRequest in bytes.", + "format": "int32" + }, + "whitelistedKeySpecs": { + "type": "array", + "description": "DNSSEC algorithm and key length types that can be used for DnsKeys.", + "items": { + "$ref": "DnsKeySpec" + } + } + } + }, + "ResourceRecordSet": { + "id": "ResourceRecordSet", + "type": "object", + "description": "A unit of data that will be returned by the DNS servers.", + "properties": { + "kind": { + "type": "string", + "description": "Identifies what kind of resource this is. Value: the fixed string \"dns#resourceRecordSet\".", + "default": "dns#resourceRecordSet" + }, + "name": { + "type": "string", + "description": "For example, www.example.com." + }, + "rrdatas": { + "type": "array", + "description": "As defined in RFC 1035 (section 5) and RFC 1034 (section 3.6.1).", + "items": { + "type": "string" + } + }, + "signatureRrdatas": { + "type": "array", + "description": "As defined in RFC 4034 (section 3.2).", + "items": { + "type": "string" + } + }, + "ttl": { + "type": "integer", + "description": "Number of seconds that this ResourceRecordSet can be cached by resolvers.", + "format": "int32" + }, + "type": { + "type": "string", + "description": "The identifier of a supported record type, for example, A, AAAA, MX, TXT, and so on." + } + } + }, + "ResourceRecordSetsListResponse": { + "id": "ResourceRecordSetsListResponse", + "type": "object", + "properties": { + "header": { + "$ref": "ResponseHeader" + }, + "kind": { + "type": "string", + "description": "Type of resource.", + "default": "dns#resourceRecordSetsListResponse" + }, + "nextPageToken": { + "type": "string", + "description": "The presence of this field indicates that there exist more results following your last page of results in pagination order. To fetch them, make another list request using this value as your pagination token.\n\nIn this way you can retrieve the complete contents of even very large collections one page at a time. However, if the contents of the collection change between the first and last paginated list request, the set of all elements returned will be an inconsistent view of the collection. There is no way to retrieve a consistent snapshot of a collection larger than the maximum page size." + }, + "rrsets": { + "type": "array", + "description": "The resource record set resources.", + "items": { + "$ref": "ResourceRecordSet" + } + } + } + }, + "ResponseHeader": { + "id": "ResponseHeader", + "type": "object", + "description": "Elements common to every response.", + "properties": { + "operationId": { + "type": "string", + "description": "For mutating operation requests that completed successfully. This is the client_operation_id if the client specified it, otherwise it is generated by the server (output only)." + } + } + } + }, + "resources": { + "changes": { + "methods": { + "create": { + "id": "dns.changes.create", + "path": "{project}/managedZones/{managedZone}/changes", + "httpMethod": "POST", + "description": "Atomically update the ResourceRecordSet collection.", + "parameters": { + "clientOperationId": { + "type": "string", + "description": "For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.", + "location": "query" + }, + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request. Can be the managed zone name or id.", + "required": true, + "location": "path" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project", + "managedZone" + ], + "request": { + "$ref": "Change" + }, + "response": { + "$ref": "Change" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + }, + "get": { + "id": "dns.changes.get", + "path": "{project}/managedZones/{managedZone}/changes/{changeId}", + "httpMethod": "GET", + "description": "Fetch the representation of an existing Change.", + "parameters": { + "changeId": { + "type": "string", + "description": "The identifier of the requested change, from a previous ResourceRecordSetsChangeResponse.", + "required": true, + "location": "path" + }, + "clientOperationId": { + "type": "string", + "description": "For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.", + "location": "query" + }, + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request. Can be the managed zone name or id.", + "required": true, + "location": "path" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project", + "managedZone", + "changeId" + ], + "response": { + "$ref": "Change" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/ndev.clouddns.readonly", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + }, + "list": { + "id": "dns.changes.list", + "path": "{project}/managedZones/{managedZone}/changes", + "httpMethod": "GET", + "description": "Enumerate Changes to a ResourceRecordSet collection.", + "parameters": { + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request. Can be the managed zone name or id.", + "required": true, + "location": "path" + }, + "maxResults": { + "type": "integer", + "description": "Optional. Maximum number of results to be returned. If unspecified, the server will decide how many results to return.", + "format": "int32", + "location": "query" + }, + "pageToken": { + "type": "string", + "description": "Optional. A tag returned by a previous list request that was truncated. Use this parameter to continue a previous list request.", + "location": "query" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + }, + "sortBy": { + "type": "string", + "description": "Sorting criterion. The only supported value is change sequence.", + "default": "changeSequence", + "enum": [ + "changeSequence" + ], + "enumDescriptions": [ + "" + ], + "location": "query" + }, + "sortOrder": { + "type": "string", + "description": "Sorting order direction: 'ascending' or 'descending'.", + "location": "query" + } + }, + "parameterOrder": [ + "project", + "managedZone" + ], + "response": { + "$ref": "ChangesListResponse" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/ndev.clouddns.readonly", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + } + } + }, + "dnsKeys": { + "methods": { + "get": { + "id": "dns.dnsKeys.get", + "path": "{project}/managedZones/{managedZone}/dnsKeys/{dnsKeyId}", + "httpMethod": "GET", + "description": "Fetch the representation of an existing DnsKey.", + "parameters": { + "clientOperationId": { + "type": "string", + "description": "For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.", + "location": "query" + }, + "digestType": { + "type": "string", + "description": "An optional comma-separated list of digest types to compute and display for key signing keys. If omitted, the recommended digest type will be computed and displayed.", + "location": "query" + }, + "dnsKeyId": { + "type": "string", + "description": "The identifier of the requested DnsKey.", + "required": true, + "location": "path" + }, + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request. Can be the managed zone name or id.", + "required": true, + "location": "path" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project", + "managedZone", + "dnsKeyId" + ], + "response": { + "$ref": "DnsKey" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/ndev.clouddns.readonly", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + }, + "list": { + "id": "dns.dnsKeys.list", + "path": "{project}/managedZones/{managedZone}/dnsKeys", + "httpMethod": "GET", + "description": "Enumerate DnsKeys to a ResourceRecordSet collection.", + "parameters": { + "digestType": { + "type": "string", + "description": "An optional comma-separated list of digest types to compute and display for key signing keys. If omitted, the recommended digest type will be computed and displayed.", + "location": "query" + }, + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request. Can be the managed zone name or id.", + "required": true, + "location": "path" + }, + "maxResults": { + "type": "integer", + "description": "Optional. Maximum number of results to be returned. If unspecified, the server will decide how many results to return.", + "format": "int32", + "location": "query" + }, + "pageToken": { + "type": "string", + "description": "Optional. A tag returned by a previous list request that was truncated. Use this parameter to continue a previous list request.", + "location": "query" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project", + "managedZone" + ], + "response": { + "$ref": "DnsKeysListResponse" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/ndev.clouddns.readonly", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + } + } + }, + "managedZoneOperations": { + "methods": { + "get": { + "id": "dns.managedZoneOperations.get", + "path": "{project}/managedZones/{managedZone}/operations/{operation}", + "httpMethod": "GET", + "description": "Fetch the representation of an existing Operation.", + "parameters": { + "clientOperationId": { + "type": "string", + "description": "For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.", + "location": "query" + }, + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request.", + "required": true, + "location": "path" + }, + "operation": { + "type": "string", + "description": "Identifies the operation addressed by this request.", + "required": true, + "location": "path" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project", + "managedZone", + "operation" + ], + "response": { + "$ref": "Operation" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/ndev.clouddns.readonly", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + }, + "list": { + "id": "dns.managedZoneOperations.list", + "path": "{project}/managedZones/{managedZone}/operations", + "httpMethod": "GET", + "description": "Enumerate Operations for the given ManagedZone.", + "parameters": { + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request.", + "required": true, + "location": "path" + }, + "maxResults": { + "type": "integer", + "description": "Optional. Maximum number of results to be returned. If unspecified, the server will decide how many results to return.", + "format": "int32", + "location": "query" + }, + "pageToken": { + "type": "string", + "description": "Optional. A tag returned by a previous list request that was truncated. Use this parameter to continue a previous list request.", + "location": "query" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + }, + "sortBy": { + "type": "string", + "description": "Sorting criterion. The only supported values are START_TIME and ID.", + "default": "startTime", + "enum": [ + "id", + "startTime" + ], + "enumDescriptions": [ + "", + "" + ], + "location": "query" + } + }, + "parameterOrder": [ + "project", + "managedZone" + ], + "response": { + "$ref": "ManagedZoneOperationsListResponse" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/ndev.clouddns.readonly", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + } + } + }, + "managedZones": { + "methods": { + "create": { + "id": "dns.managedZones.create", + "path": "{project}/managedZones", + "httpMethod": "POST", + "description": "Create a new ManagedZone.", + "parameters": { + "clientOperationId": { + "type": "string", + "description": "For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.", + "location": "query" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project" + ], + "request": { + "$ref": "ManagedZone" + }, + "response": { + "$ref": "ManagedZone" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + }, + "delete": { + "id": "dns.managedZones.delete", + "path": "{project}/managedZones/{managedZone}", + "httpMethod": "DELETE", + "description": "Delete a previously created ManagedZone.", + "parameters": { + "clientOperationId": { + "type": "string", + "description": "For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.", + "location": "query" + }, + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request. Can be the managed zone name or id.", + "required": true, + "location": "path" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project", + "managedZone" + ], + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + }, + "get": { + "id": "dns.managedZones.get", + "path": "{project}/managedZones/{managedZone}", + "httpMethod": "GET", + "description": "Fetch the representation of an existing ManagedZone.", + "parameters": { + "clientOperationId": { + "type": "string", + "description": "For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.", + "location": "query" + }, + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request. Can be the managed zone name or id.", + "required": true, + "location": "path" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project", + "managedZone" + ], + "response": { + "$ref": "ManagedZone" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/ndev.clouddns.readonly", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + }, + "list": { + "id": "dns.managedZones.list", + "path": "{project}/managedZones", + "httpMethod": "GET", + "description": "Enumerate ManagedZones that have been created but not yet deleted.", + "parameters": { + "dnsName": { + "type": "string", + "description": "Restricts the list to return only zones with this domain name.", + "location": "query" + }, + "maxResults": { + "type": "integer", + "description": "Optional. Maximum number of results to be returned. If unspecified, the server will decide how many results to return.", + "format": "int32", + "location": "query" + }, + "pageToken": { + "type": "string", + "description": "Optional. A tag returned by a previous list request that was truncated. Use this parameter to continue a previous list request.", + "location": "query" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project" + ], + "response": { + "$ref": "ManagedZonesListResponse" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/ndev.clouddns.readonly", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + }, + "patch": { + "id": "dns.managedZones.patch", + "path": "{project}/managedZones/{managedZone}", + "httpMethod": "PATCH", + "description": "Update an existing ManagedZone. This method supports patch semantics.", + "parameters": { + "clientOperationId": { + "type": "string", + "description": "For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.", + "location": "query" + }, + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request. Can be the managed zone name or id.", + "required": true, + "location": "path" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project", + "managedZone" + ], + "request": { + "$ref": "ManagedZone" + }, + "response": { + "$ref": "Operation" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + }, + "update": { + "id": "dns.managedZones.update", + "path": "{project}/managedZones/{managedZone}", + "httpMethod": "PUT", + "description": "Update an existing ManagedZone.", + "parameters": { + "clientOperationId": { + "type": "string", + "description": "For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.", + "location": "query" + }, + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request. Can be the managed zone name or id.", + "required": true, + "location": "path" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project", + "managedZone" + ], + "request": { + "$ref": "ManagedZone" + }, + "response": { + "$ref": "Operation" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + } + } + }, + "projects": { + "methods": { + "get": { + "id": "dns.projects.get", + "path": "{project}", + "httpMethod": "GET", + "description": "Fetch the representation of an existing Project.", + "parameters": { + "clientOperationId": { + "type": "string", + "description": "For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.", + "location": "query" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + } + }, + "parameterOrder": [ + "project" + ], + "response": { + "$ref": "Project" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/ndev.clouddns.readonly", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + } + } + }, + "resourceRecordSets": { + "methods": { + "list": { + "id": "dns.resourceRecordSets.list", + "path": "{project}/managedZones/{managedZone}/rrsets", + "httpMethod": "GET", + "description": "Enumerate ResourceRecordSets that have been created but not yet deleted.", + "parameters": { + "managedZone": { + "type": "string", + "description": "Identifies the managed zone addressed by this request. Can be the managed zone name or id.", + "required": true, + "location": "path" + }, + "maxResults": { + "type": "integer", + "description": "Optional. Maximum number of results to be returned. If unspecified, the server will decide how many results to return.", + "format": "int32", + "location": "query" + }, + "name": { + "type": "string", + "description": "Restricts the list to return only records with this fully qualified domain name.", + "location": "query" + }, + "pageToken": { + "type": "string", + "description": "Optional. A tag returned by a previous list request that was truncated. Use this parameter to continue a previous list request.", + "location": "query" + }, + "project": { + "type": "string", + "description": "Identifies the project addressed by this request.", + "required": true, + "location": "path" + }, + "type": { + "type": "string", + "description": "Restricts the list to return only records of this type. If present, the \"name\" parameter must also be present.", + "location": "query" + } + }, + "parameterOrder": [ + "project", + "managedZone" + ], + "response": { + "$ref": "ResourceRecordSetsListResponse" + }, + "scopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + "https://www.googleapis.com/auth/ndev.clouddns.readonly", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite" + ] + } + } + } + } +} diff --git a/certbot-dns-google/docs/index.rst b/certbot-dns-google/docs/index.rst index a8a322f97..6bb82c76a 100644 --- a/certbot-dns-google/docs/index.rst +++ b/certbot-dns-google/docs/index.rst @@ -10,14 +10,14 @@ Welcome to certbot-dns-google's documentation! :maxdepth: 2 :caption: Contents: +.. automodule:: certbot_dns_google + :members: + .. toctree:: :maxdepth: 1 api -.. automodule:: certbot_dns_google - :members: - Indices and tables diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index 0dfff0402..6c25ed452 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-luadns/docs/index.rst b/certbot-dns-luadns/docs/index.rst index 589e925c0..d8cbdeb3f 100644 --- a/certbot-dns-luadns/docs/index.rst +++ b/certbot-dns-luadns/docs/index.rst @@ -10,14 +10,14 @@ Welcome to certbot-dns-luadns's documentation! :maxdepth: 2 :caption: Contents: +.. automodule:: certbot_dns_luadns + :members: + .. toctree:: :maxdepth: 1 api -.. automodule:: certbot_dns_luadns - :members: - Indices and tables diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index b255691dc..f872d7093 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -4,14 +4,14 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. install_requires = [ 'acme>=0.21.1', 'certbot>=0.21.1', - 'dns-lexicon', + 'dns-lexicon>=2.2.1', # Support for >1 TXT record per name 'mock', 'setuptools', 'zope.interface', diff --git a/certbot-dns-nsone/docs/index.rst b/certbot-dns-nsone/docs/index.rst index 6abba81ec..bc204a6ff 100644 --- a/certbot-dns-nsone/docs/index.rst +++ b/certbot-dns-nsone/docs/index.rst @@ -10,14 +10,14 @@ Welcome to certbot-dns-nsone's documentation! :maxdepth: 2 :caption: Contents: +.. automodule:: certbot_dns_nsone + :members: + .. toctree:: :maxdepth: 1 api -.. automodule:: certbot_dns_nsone - :members: - Indices and tables diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index 68d8f6cdb..102ed48c2 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,14 +4,14 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. install_requires = [ 'acme>=0.21.1', 'certbot>=0.21.1', - 'dns-lexicon', + 'dns-lexicon>=2.2.1', # Support for >1 TXT record per name 'mock', 'setuptools', 'zope.interface', diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py index 85a3bf9bb..127773469 100644 --- a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py @@ -70,11 +70,11 @@ class Authenticator(dns_common.DNSAuthenticator): self._validate_algorithm ) - def _perform(self, domain, validation_name, validation): - self._get_rfc2136_client().add_txt_record(domain, validation_name, validation, self.ttl) + def _perform(self, _domain, validation_name, validation): + self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl) - def _cleanup(self, domain, validation_name, validation): - self._get_rfc2136_client().del_txt_record(domain, validation_name, validation) + def _cleanup(self, _domain, validation_name, validation): + self._get_rfc2136_client().del_txt_record(validation_name, validation) def _get_rfc2136_client(self): return _RFC2136Client(self.credentials.conf('server'), @@ -95,18 +95,17 @@ class _RFC2136Client(object): }) self.algorithm = key_algorithm - def add_txt_record(self, domain_name, record_name, record_content, record_ttl): + def add_txt_record(self, record_name, record_content, record_ttl): """ Add a TXT record using the supplied information. - :param str domain: The domain to use to find the closest SOA. :param str record_name: The record name (typically beginning with '_acme-challenge.'). :param str record_content: The record content (typically the challenge validation). :param int record_ttl: The record TTL (number of seconds that the record may be cached). :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server """ - domain = self._find_domain(domain_name) + domain = self._find_domain(record_name) n = dns.name.from_text(record_name) o = dns.name.from_text(domain) @@ -131,18 +130,17 @@ class _RFC2136Client(object): raise errors.PluginError('Received response from server: {0}' .format(dns.rcode.to_text(rcode))) - def del_txt_record(self, domain_name, record_name, record_content): + def del_txt_record(self, record_name, record_content): """ Delete a TXT record using the supplied information. - :param str domain: The domain to use to find the closest SOA. :param str record_name: The record name (typically beginning with '_acme-challenge.'). :param str record_content: The record content (typically the challenge validation). :param int record_ttl: The record TTL (number of seconds that the record may be cached). :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server """ - domain = self._find_domain(domain_name) + domain = self._find_domain(record_name) n = dns.name.from_text(record_name) o = dns.name.from_text(domain) @@ -167,17 +165,17 @@ class _RFC2136Client(object): raise errors.PluginError('Received response from server: {0}' .format(dns.rcode.to_text(rcode))) - def _find_domain(self, domain_name): + def _find_domain(self, record_name): """ Find the closest domain with an SOA record for a given domain name. - :param str domain_name: The domain name for which to find the closest SOA record. + :param str record_name: The record name for which to find the closest SOA record. :returns: The domain, if found. :rtype: str :raises certbot.errors.PluginError: if no SOA record can be found. """ - domain_name_guesses = dns_common.base_domain_name_guesses(domain_name) + domain_name_guesses = dns_common.base_domain_name_guesses(record_name) # Loop through until we find an authoritative SOA record for guess in domain_name_guesses: @@ -185,7 +183,7 @@ class _RFC2136Client(object): return guess raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.' - .format(domain_name, domain_name_guesses)) + .format(record_name, domain_name_guesses)) def _query_soa(self, domain_name): """ diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py index 54fdb8575..8a5166330 100644 --- a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py @@ -41,7 +41,7 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic def test_perform(self): self.auth.perform([self.achall]) - expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)] + expected = [mock.call.add_txt_record('_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)] self.assertEqual(expected, self.mock_client.mock_calls) def test_cleanup(self): @@ -49,7 +49,7 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic self.auth._attempt_cleanup = True self.auth.cleanup([self.achall]) - expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)] + expected = [mock.call.del_txt_record('_acme-challenge.'+DOMAIN, mock.ANY)] self.assertEqual(expected, self.mock_client.mock_calls) def test_invalid_algorithm_raises(self): @@ -82,7 +82,7 @@ class RFC2136ClientTest(unittest.TestCase): # _find_domain | pylint: disable=protected-access self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") - self.rfc2136_client.add_txt_record(DOMAIN, "bar", "baz", 42) + self.rfc2136_client.add_txt_record("bar", "baz", 42) query_mock.assert_called_with(mock.ANY, SERVER) self.assertTrue("bar. 42 IN TXT \"baz\"" in str(query_mock.call_args[0][0])) @@ -96,7 +96,7 @@ class RFC2136ClientTest(unittest.TestCase): self.assertRaises( errors.PluginError, self.rfc2136_client.add_txt_record, - DOMAIN, "bar", "baz", 42) + "bar", "baz", 42) @mock.patch("dns.query.tcp") def test_add_txt_record_server_error(self, query_mock): @@ -107,7 +107,7 @@ class RFC2136ClientTest(unittest.TestCase): self.assertRaises( errors.PluginError, self.rfc2136_client.add_txt_record, - DOMAIN, "bar", "baz", 42) + "bar", "baz", 42) @mock.patch("dns.query.tcp") def test_del_txt_record(self, query_mock): @@ -115,7 +115,7 @@ class RFC2136ClientTest(unittest.TestCase): # _find_domain | pylint: disable=protected-access self.rfc2136_client._find_domain = mock.MagicMock(return_value="example.com") - self.rfc2136_client.del_txt_record(DOMAIN, "bar", "baz") + self.rfc2136_client.del_txt_record("bar", "baz") query_mock.assert_called_with(mock.ANY, SERVER) self.assertTrue("bar. 0 NONE TXT \"baz\"" in str(query_mock.call_args[0][0])) @@ -129,7 +129,7 @@ class RFC2136ClientTest(unittest.TestCase): self.assertRaises( errors.PluginError, self.rfc2136_client.del_txt_record, - DOMAIN, "bar", "baz") + "bar", "baz") @mock.patch("dns.query.tcp") def test_del_txt_record_server_error(self, query_mock): @@ -140,7 +140,7 @@ class RFC2136ClientTest(unittest.TestCase): self.assertRaises( errors.PluginError, self.rfc2136_client.del_txt_record, - DOMAIN, "bar", "baz") + "bar", "baz") def test_find_domain(self): # _query_soa | pylint: disable=protected-access diff --git a/certbot-dns-rfc2136/docs/index.rst b/certbot-dns-rfc2136/docs/index.rst index 71705cb7f..c2d4daafe 100644 --- a/certbot-dns-rfc2136/docs/index.rst +++ b/certbot-dns-rfc2136/docs/index.rst @@ -10,14 +10,14 @@ Welcome to certbot-dns-rfc2136's documentation! :maxdepth: 2 :caption: Contents: +.. automodule:: certbot_dns_rfc2136 + :members: + .. toctree:: :maxdepth: 1 api -.. automodule:: certbot_dns_rfc2136 - :members: - Indices and tables diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index 3d6b3799b..2cbc29e6d 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-dns-route53/docs/index.rst b/certbot-dns-route53/docs/index.rst index bacf73150..fe5adfad5 100644 --- a/certbot-dns-route53/docs/index.rst +++ b/certbot-dns-route53/docs/index.rst @@ -10,14 +10,14 @@ Welcome to certbot-dns-route53's documentation! :maxdepth: 2 :caption: Contents: +.. automodule:: certbot_dns_route53 + :members: + .. toctree:: :maxdepth: 1 api -.. automodule:: certbot_dns_route53 - :members: - Indices and tables diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index ad20725b5..3f21c4dc5 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -3,7 +3,7 @@ import sys from distutils.core import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-nginx/certbot_nginx/configurator.py b/certbot-nginx/certbot_nginx/configurator.py index 83e308bac..13fe493fc 100644 --- a/certbot-nginx/certbot_nginx/configurator.py +++ b/certbot-nginx/certbot_nginx/configurator.py @@ -61,6 +61,9 @@ class NginxConfigurator(common.Installer): DEFAULT_LISTEN_PORT = '80' + # SSL directives that Certbot can add when installing a new certificate. + SSL_DIRECTIVES = ['ssl_certificate', 'ssl_certificate_key', 'ssl_dhparam'] + @classmethod def add_parser_arguments(cls, add): add("server-root", default=constants.CLI_DEFAULTS["server_root"], @@ -105,6 +108,7 @@ class NginxConfigurator(common.Installer): self.parser = None self.version = version self._enhance_func = {"redirect": self._enable_redirect, + "ensure-http-header": self._set_http_header, "staple-ocsp": self._enable_ocsp_stapling} self.reverter.recovery_routine() @@ -188,8 +192,8 @@ class NginxConfigurator(common.Installer): cert_directives = [['\n ', 'ssl_certificate', ' ', fullchain_path], ['\n ', 'ssl_certificate_key', ' ', key_path]] - self.parser.add_server_directives(vhost, - cert_directives, replace=True) + self.parser.update_or_add_server_directives(vhost, + cert_directives) logger.info("Deploying Certificate to VirtualHost %s", vhost.filep) self.save_notes += ("Changed vhost at %s with addresses of %s\n" % @@ -328,7 +332,8 @@ class NginxConfigurator(common.Installer): def _vhost_from_duplicated_default(self, domain, port=None): if self.new_vhost is None: default_vhost = self._get_default_vhost(port) - self.new_vhost = self.parser.duplicate_vhost(default_vhost, delete_default=True) + self.new_vhost = self.parser.duplicate_vhost(default_vhost, + remove_singleton_listen_params=True) self.new_vhost.names = set() self._add_server_name_to_vhost(self.new_vhost, domain) @@ -340,7 +345,7 @@ class NginxConfigurator(common.Installer): for name in vhost.names: name_block[0].append(' ') name_block[0].append(name) - self.parser.add_server_directives(vhost, name_block, replace=True) + self.parser.update_or_add_server_directives(vhost, name_block) def _get_default_vhost(self, port): vhost_list = self.parser.get_vhosts() @@ -580,7 +585,7 @@ class NginxConfigurator(common.Installer): # have it continue to do so. if len(vhost.addrs) == 0: listen_block = [['\n ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]] - self.parser.add_server_directives(vhost, listen_block, replace=False) + self.parser.add_server_directives(vhost, listen_block) if vhost.ipv6_enabled(): ipv6_block = ['\n ', @@ -614,14 +619,14 @@ class NginxConfigurator(common.Installer): ]) self.parser.add_server_directives( - vhost, ssl_block, replace=False) + vhost, ssl_block) ################################## # enhancement methods (IInstaller) ################################## def supported_enhancements(self): # pylint: disable=no-self-use """Returns currently supported enhancements.""" - return ['redirect', 'staple-ocsp'] + return ['redirect', 'ensure-http-header', 'staple-ocsp'] def enhance(self, domain, enhancement, options=None): """Enhance configuration. @@ -647,13 +652,80 @@ class NginxConfigurator(common.Installer): test_redirect_block = _test_block_from_block(_redirect_block_for_domain(domain)) return vhost.contains_list(test_redirect_block) + def _set_http_header(self, domain, header_substring): + """Enables header identified by header_substring on domain. + + If the vhost is listening plaintextishly, separates out the relevant + directives into a new server block, and only add header directive to + HTTPS block. + + :param str domain: the domain to enable header for. + :param str header_substring: String to uniquely identify a header. + e.g. Strict-Transport-Security, Upgrade-Insecure-Requests + :returns: Success + :raises .errors.PluginError: If no viable HTTPS host can be created or + set with header header_substring. + """ + vhosts = self.choose_vhosts(domain) + if not vhosts: + raise errors.PluginError( + "Unable to find corresponding HTTPS host for enhancement.") + for vhost in vhosts: + if vhost.has_header(header_substring): + raise errors.PluginEnhancementAlreadyPresent( + "Existing %s header" % (header_substring)) + + # if there is no separate SSL block, break the block into two and + # choose the SSL block. + if vhost.ssl and any([not addr.ssl for addr in vhost.addrs]): + _, vhost = self._split_block(vhost) + + header_directives = [ + ['\n ', 'add_header', ' ', header_substring, ' '] + + constants.HEADER_ARGS[header_substring], + ['\n']] + self.parser.add_server_directives(vhost, header_directives) + def _add_redirect_block(self, vhost, domain): """Add redirect directive to vhost """ redirect_block = _redirect_block_for_domain(domain) self.parser.add_server_directives( - vhost, redirect_block, replace=False, insert_at_top=True) + vhost, redirect_block, insert_at_top=True) + + def _split_block(self, vhost, only_directives=None): + """Splits this "virtual host" (i.e. this nginx server block) into + separate HTTP and HTTPS blocks. + + :param vhost: The server block to break up into two. + :param list only_directives: If this exists, only duplicate these directives + when splitting the block. + :type vhost: :class:`~certbot_nginx.obj.VirtualHost` + :returns: tuple (http_vhost, https_vhost) + :rtype: tuple of type :class:`~certbot_nginx.obj.VirtualHost` + """ + http_vhost = self.parser.duplicate_vhost(vhost, only_directives=only_directives) + + def _ssl_match_func(directive): + return 'ssl' in directive + + def _ssl_config_match_func(directive): + return self.mod_ssl_conf in directive + + def _no_ssl_match_func(directive): + return 'ssl' not in directive + + # remove all ssl addresses and related directives from the new block + for directive in self.SSL_DIRECTIVES: + self.parser.remove_server_directives(http_vhost, directive) + self.parser.remove_server_directives(http_vhost, 'listen', match_func=_ssl_match_func) + self.parser.remove_server_directives(http_vhost, 'include', + match_func=_ssl_config_match_func) + + # remove all non-ssl addresses from the existing block + self.parser.remove_server_directives(vhost, 'listen', match_func=_no_ssl_match_func) + return http_vhost, vhost def _enable_redirect(self, domain, unused_options): """Redirect all equivalent HTTP traffic to ssl_vhost. @@ -694,28 +766,15 @@ class NginxConfigurator(common.Installer): :param `~obj.Vhost` vhost: vhost to enable redirect for """ - new_vhost = None + http_vhost = None if vhost.ssl: - new_vhost = self.parser.duplicate_vhost(vhost, - only_directives=['listen', 'server_name']) - - def _ssl_match_func(directive): - return 'ssl' in directive - - def _no_ssl_match_func(directive): - return 'ssl' not in directive - - # remove all ssl addresses from the new block - self.parser.remove_server_directives(new_vhost, 'listen', match_func=_ssl_match_func) - - # remove all non-ssl addresses from the existing block - self.parser.remove_server_directives(vhost, 'listen', match_func=_no_ssl_match_func) + http_vhost, _ = self._split_block(vhost, ['listen', 'server_name']) # Add this at the bottom to get the right order of directives return_404_directive = [['\n ', 'return', ' ', '404']] - self.parser.add_server_directives(new_vhost, return_404_directive, replace=False) + self.parser.add_server_directives(http_vhost, return_404_directive) - vhost = new_vhost + vhost = http_vhost if self._has_certbot_redirect(vhost, domain): logger.info("Traffic on port %s already redirecting to ssl in %s", @@ -763,7 +822,7 @@ class NginxConfigurator(common.Installer): try: self.parser.add_server_directives(vhost, - stapling_directives, replace=False) + stapling_directives) except errors.MisconfigurationError as error: logger.debug(error) raise errors.PluginError("An error occurred while enabling OCSP " @@ -837,7 +896,7 @@ class NginxConfigurator(common.Installer): raise errors.PluginError( "Unable to run %s -V" % self.conf('ctl')) - version_regex = re.compile(r"nginx/([0-9\.]*)", re.IGNORECASE) + version_regex = re.compile(r"nginx version: ([^/]+)/([0-9\.]*)", re.IGNORECASE) version_matches = version_regex.findall(text) sni_regex = re.compile(r"TLS SNI support enabled", re.IGNORECASE) @@ -854,7 +913,12 @@ class NginxConfigurator(common.Installer): if not sni_matches: raise errors.PluginError("Nginx build doesn't support SNI") - nginx_version = tuple([int(i) for i in version_matches[0].split(".")]) + product_name, product_version = version_matches[0] + if product_name is not 'nginx': + logger.warning("NGINX derivative %s is not officially supported by" + " certbot", product_name) + + nginx_version = tuple([int(i) for i in product_version.split(".")]) # nginx < 0.8.48 uses machine hostname as default server_name instead of # the empty string diff --git a/certbot-nginx/certbot_nginx/constants.py b/certbot-nginx/certbot_nginx/constants.py index 2e72b8686..3f263fea3 100644 --- a/certbot-nginx/certbot_nginx/constants.py +++ b/certbot-nginx/certbot_nginx/constants.py @@ -44,3 +44,7 @@ def os_constant(key): :return: value of constant for active os """ return CLI_DEFAULTS[key] + +HSTS_ARGS = ['\"max-age=31536000\"', ' ', 'always'] + +HEADER_ARGS = {'Strict-Transport-Security': HSTS_ARGS} diff --git a/certbot-nginx/certbot_nginx/http_01.py b/certbot-nginx/certbot_nginx/http_01.py index 0b1b2bfe0..d08a3b1cb 100644 --- a/certbot-nginx/certbot_nginx/http_01.py +++ b/certbot-nginx/certbot_nginx/http_01.py @@ -159,16 +159,22 @@ class NginxHttp01(common.ChallengePerformer): document_root = os.path.join( self.configurator.config.work_dir, "http_01_nonexistent") + block.extend([['server_name', ' ', achall.domain], + ['root', ' ', document_root], + self._location_directive_for_achall(achall) + ]) + # TODO: do we want to return something else if they otherwise access this block? + return [['server'], block] + + def _location_directive_for_achall(self, achall): validation = achall.validation(achall.account_key) validation_path = self._get_validation_path(achall) - block.extend([['server_name', ' ', achall.domain], - ['root', ' ', document_root], - [['location', ' ', '=', ' ', validation_path], - [['default_type', ' ', 'text/plain'], - ['return', ' ', '200', ' ', validation]]]]) - # TODO: do we want to return something else if they otherwise access this block? - return [['server'], block] + location_directive = [['location', ' ', '=', ' ', validation_path], + [['default_type', ' ', 'text/plain'], + ['return', ' ', '200', ' ', validation]]] + return location_directive + def _make_or_mod_server_block(self, achall): """Modifies a server block to respond to a challenge. @@ -191,17 +197,12 @@ class NginxHttp01(common.ChallengePerformer): vhost = vhosts[0] # Modify existing server block - validation = achall.validation(achall.account_key) - validation_path = self._get_validation_path(achall) - - location_directive = [[['location', ' ', '=', ' ', validation_path], - [['default_type', ' ', 'text/plain'], - ['return', ' ', '200', ' ', validation]]]] + location_directive = [self._location_directive_for_achall(achall)] self.configurator.parser.add_server_directives(vhost, - location_directive, replace=False) + location_directive) rewrite_directive = [['rewrite', ' ', '^(/.well-known/acme-challenge/.*)', ' ', '$1', ' ', 'break']] self.configurator.parser.add_server_directives(vhost, - rewrite_directive, replace=False, insert_at_top=True) + rewrite_directive, insert_at_top=True) diff --git a/certbot-nginx/certbot_nginx/obj.py b/certbot-nginx/certbot_nginx/obj.py index 3625a95b9..8868fcfad 100644 --- a/certbot-nginx/certbot_nginx/obj.py +++ b/certbot-nginx/certbot_nginx/obj.py @@ -5,7 +5,7 @@ import six from certbot.plugins import common -REDIRECT_DIRECTIVES = ['return', 'rewrite'] +ADD_HEADER_DIRECTIVE = 'add_header' class Addr(common.Addr): r"""Represents an Nginx address, i.e. what comes after the 'listen' @@ -29,6 +29,8 @@ class Addr(common.Addr): :param str port: port number or "\*" or "" :param bool ssl: Whether the directive includes 'ssl' :param bool default: Whether the directive includes 'default_server' + :param bool default: Whether this is an IPv6 address + :param bool ipv6only: Whether the directive includes 'ipv6only=on' """ UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0') @@ -88,6 +90,8 @@ class Addr(common.Addr): ssl = True elif nextpart == 'default_server': default = True + elif nextpart == 'default': + default = True elif nextpart == "ipv6only=on": ipv6only = True @@ -198,6 +202,14 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods tuple(self.addrs), tuple(self.names), self.ssl, self.enabled)) + def has_header(self, header_name): + """Determine if this server block has a particular header set. + :param str header_name: The name of the header to check for, e.g. + 'Strict-Transport-Security' + """ + found = _find_directive(self.raw, ADD_HEADER_DIRECTIVE, header_name) + return found is not None + def contains_list(self, test): """Determine if raw server block contains test list at top level """ @@ -233,3 +245,19 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods addrs=", ".join(str(addr) for addr in self.addrs), names=", ".join(self.names), https="Yes" if self.ssl else "No")) + +def _find_directive(directives, directive_name, match_content=None): + """Find a directive of type directive_name in directives. If match_content is given, + Searches for `match_content` in the directive arguments. + """ + if not directives or isinstance(directives, six.string_types) or len(directives) == 0: + return None + + # If match_content is None, just match on directive type. Otherwise, match on + # both directive type -and- the content! + if directives[0] == directive_name and \ + (match_content is None or match_content in directives): + return directives + + matches = (_find_directive(line, directive_name, match_content) for line in directives) + return next((m for m in matches if m is not None), None) diff --git a/certbot-nginx/certbot_nginx/parser.py b/certbot-nginx/certbot_nginx/parser.py index fbd6c0ade..577e783fc 100644 --- a/certbot-nginx/certbot_nginx/parser.py +++ b/certbot-nginx/certbot_nginx/parser.py @@ -276,15 +276,13 @@ class NginxParser(object): return False - def add_server_directives(self, vhost, directives, replace, insert_at_top=False): - """Add or replace directives in the server block identified by vhost. + def add_server_directives(self, vhost, directives, insert_at_top=False): + """Add directives to the server block identified by vhost. This method modifies vhost to be fully consistent with the new directives. - ..note :: If replace is True and the directive already exists, the first - instance will be replaced. Otherwise, the directive is added. - ..note :: If replace is False nothing gets added if an identical - block exists already. + ..note :: It's an error to try and add a nonrepeatable directive that already + exists in the config block with a conflicting value. ..todo :: Doesn't match server blocks whose server_name directives are split across multiple conf files. @@ -292,13 +290,34 @@ class NginxParser(object): :param :class:`~certbot_nginx.obj.VirtualHost` vhost: The vhost whose information we use to match on :param list directives: The directives to add - :param bool replace: Whether to only replace existing directives :param bool insert_at_top: True if the directives need to be inserted at the top of the server block instead of the bottom """ self._modify_server_directives(vhost, - functools.partial(_add_directives, directives, replace, insert_at_top)) + functools.partial(_add_directives, directives, insert_at_top)) + + def update_or_add_server_directives(self, vhost, directives, insert_at_top=False): + """Add or replace directives in the server block identified by vhost. + + This method modifies vhost to be fully consistent with the new directives. + + ..note :: When a directive with the same name already exists in the + config block, the first instance will be replaced. Otherwise, the directive + will be appended/prepended to the config block as in add_server_directives. + + ..todo :: Doesn't match server blocks whose server_name directives are + split across multiple conf files. + + :param :class:`~certbot_nginx.obj.VirtualHost` vhost: The vhost + whose information we use to match on + :param list directives: The directives to add + :param bool insert_at_top: True if the directives need to be inserted at the top + of the server block instead of the bottom + + """ + self._modify_server_directives(vhost, + functools.partial(_update_or_add_directives, directives, insert_at_top)) def remove_server_directives(self, vhost, directive_name, match_func=None): """Remove all directives of type directive_name. @@ -335,13 +354,14 @@ class NginxParser(object): except errors.MisconfigurationError as err: raise errors.MisconfigurationError("Problem in %s: %s" % (filename, str(err))) - def duplicate_vhost(self, vhost_template, delete_default=False, only_directives=None): + def duplicate_vhost(self, vhost_template, remove_singleton_listen_params=False, + only_directives=None): """Duplicate the vhost in the configuration files. :param :class:`~certbot_nginx.obj.VirtualHost` vhost_template: The vhost whose information we copy - :param bool delete_default: If we should remove default_server - from listen directives in the block. + :param bool remove_singleton_listen_params: If we should remove parameters + from listen directives in the block that can only be used once per address :param list only_directives: If it exists, only duplicate the named directives. Only looks at first level of depth; does not expand includes. @@ -368,15 +388,21 @@ class NginxParser(object): enclosing_block.append(raw_in_parsed) new_vhost.path[-1] = len(enclosing_block) - 1 - if delete_default: + if remove_singleton_listen_params: for addr in new_vhost.addrs: addr.default = False + addr.ipv6only = False for directive in enclosing_block[new_vhost.path[-1]][1]: - if (len(directive) > 0 and directive[0] == 'listen' - and 'default_server' in directive): - del directive[directive.index('default_server')] + if len(directive) > 0 and directive[0] == 'listen': + if 'default_server' in directive: + del directive[directive.index('default_server')] + if 'default' in directive: + del directive[directive.index('default')] + if 'ipv6only=on' in directive: + del directive[directive.index('ipv6only=on')] return new_vhost + def _parse_ssl_options(ssl_options): if ssl_options is not None: try: @@ -523,32 +549,23 @@ def _is_ssl_on_directive(entry): len(entry) == 2 and entry[0] == 'ssl' and entry[1] == 'on') -def _add_directives(directives, replace, insert_at_top, block): - """Adds or replaces directives in a config block. - - When replace=False, it's an error to try and add a nonrepeatable directive that already - exists in the config block with a conflicting value. - - When replace=True and a directive with the same name already exists in the - config block, the first instance will be replaced. Otherwise, the directive - will be added to the config block. - - ..todo :: Find directives that are in included files. - - :param list directives: The new directives. - :param bool replace: Described above. - :param bool insert_at_top: Described above. - :param list block: The block to replace in - - """ +def _add_directives(directives, insert_at_top, block): + """Adds directives to a config block.""" for directive in directives: - _add_directive(block, directive, replace, insert_at_top) + _add_directive(block, directive, insert_at_top) + if block and '\n' not in block[-1]: # could be " \n " or ["\n"] ! + block.append(nginxparser.UnspacedList('\n')) + +def _update_or_add_directives(directives, insert_at_top, block): + """Adds or replaces directives in a config block.""" + for directive in directives: + _update_or_add_directive(block, directive, insert_at_top) if block and '\n' not in block[-1]: # could be " \n " or ["\n"] ! block.append(nginxparser.UnspacedList('\n')) INCLUDE = 'include' -REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE, 'location', 'rewrite']) +REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE, 'rewrite']) COMMENT = ' managed by Certbot' COMMENT_BLOCK = [' ', '#', COMMENT] @@ -600,28 +617,20 @@ def _find_location(block, directive_name, match_func=None): return next((index for index, line in enumerate(block) \ if line and line[0] == directive_name and (match_func is None or match_func(line))), None) -def _add_directive(block, directive, replace, insert_at_top): - """Adds or replaces a single directive in a config block. +def _is_whitespace_or_comment(directive): + """Is this directive either a whitespace or comment directive?""" + return len(directive) == 0 or directive[0] == '#' - See _add_directives for more documentation. - - """ - directive = nginxparser.UnspacedList(directive) - def is_whitespace_or_comment(directive): - """Is this directive either a whitespace or comment directive?""" - return len(directive) == 0 or directive[0] == '#' - if is_whitespace_or_comment(directive): +def _add_directive(block, directive, insert_at_top): + if not isinstance(directive, nginxparser.UnspacedList): + directive = nginxparser.UnspacedList(directive) + if _is_whitespace_or_comment(directive): # whitespace or comment block.append(directive) return location = _find_location(block, directive[0]) - if replace: - if location is not None: - block[location] = directive - comment_directive(block, location) - return # Append or prepend directive. Fail if the name is not a repeatable directive name, # and there is already a copy of that directive with a different value # in the config file. @@ -646,7 +655,7 @@ def _add_directive(block, directive, replace, insert_at_top): for included_directive in included_directives: included_dir_loc = _find_location(block, included_directive[0]) included_dir_name = included_directive[0] - if not is_whitespace_or_comment(included_directive) \ + if not _is_whitespace_or_comment(included_directive) \ and not can_append(included_dir_loc, included_dir_name): if block[included_dir_loc] != included_directive: raise errors.MisconfigurationError(err_fmt.format(included_directive, @@ -667,6 +676,30 @@ def _add_directive(block, directive, replace, insert_at_top): elif block[location] != directive: raise errors.MisconfigurationError(err_fmt.format(directive, block[location])) +def _update_directive(block, directive, location): + block[location] = directive + comment_directive(block, location) + +def _update_or_add_directive(block, directive, insert_at_top): + if not isinstance(directive, nginxparser.UnspacedList): + directive = nginxparser.UnspacedList(directive) + if _is_whitespace_or_comment(directive): + # whitespace or comment + block.append(directive) + return + + location = _find_location(block, directive[0]) + + # we can update directive + if location is not None: + _update_directive(block, directive, location) + return + + _add_directive(block, directive, insert_at_top) + +def _is_certbot_comment(directive): + return '#' in directive and COMMENT in directive + def _remove_directives(directive_name, match_func, block): """Removes directives of name directive_name from a config block if match_func matches. """ @@ -674,6 +707,9 @@ def _remove_directives(directive_name, match_func, block): location = _find_location(block, directive_name, match_func=match_func) if location is None: return + # if the directive was made by us, remove the comment following + if location + 1 < len(block) and _is_certbot_comment(block[location + 1]): + del block[location + 1] del block[location] def _apply_global_addr_ssl(addr_to_ssl, parsed_server): diff --git a/certbot-nginx/certbot_nginx/tests/configurator_test.py b/certbot-nginx/certbot_nginx/tests/configurator_test.py index bffaef5e4..34abf2f0d 100644 --- a/certbot-nginx/certbot_nginx/tests/configurator_test.py +++ b/certbot-nginx/certbot_nginx/tests/configurator_test.py @@ -94,7 +94,7 @@ class NginxConfiguratorTest(util.NginxTest): "globalssl.com", "globalsslsetssl.com", "ipv6.com", "ipv6ssl.com"])) def test_supported_enhancements(self): - self.assertEqual(['redirect', 'staple-ocsp'], + self.assertEqual(['redirect', 'ensure-http-header', 'staple-ocsp'], self.config.supported_enhancements()) def test_enhance(self): @@ -113,8 +113,7 @@ class NginxConfiguratorTest(util.NginxTest): None, [0]) self.config.parser.add_server_directives( mock_vhost, - [['listen', ' ', '5001', ' ', 'ssl']], - replace=False) + [['listen', ' ', '5001', ' ', 'ssl']]) self.config.save() # pylint: disable=protected-access @@ -206,9 +205,9 @@ class NginxConfiguratorTest(util.NginxTest): "example/chain.pem", None) - @mock.patch('certbot_nginx.parser.NginxParser.add_server_directives') - def test_deploy_cert_raise_on_add_error(self, mock_add_server_directives): - mock_add_server_directives.side_effect = errors.MisconfigurationError() + @mock.patch('certbot_nginx.parser.NginxParser.update_or_add_server_directives') + def test_deploy_cert_raise_on_add_error(self, mock_update_or_add_server_directives): + mock_update_or_add_server_directives.side_effect = errors.MisconfigurationError() self.assertRaises( errors.PluginError, self.config.deploy_cert, @@ -510,6 +509,54 @@ class NginxConfiguratorTest(util.NginxTest): ['return', '404'], ['#', ' managed by Certbot'], [], [], []]]], generated_conf) + def test_split_for_headers(self): + example_conf = self.config.parser.abs_path('sites-enabled/example.com') + self.config.deploy_cert( + "example.org", + "example/cert.pem", + "example/key.pem", + "example/chain.pem", + "example/fullchain.pem") + self.config.enhance("www.example.com", "ensure-http-header", "Strict-Transport-Security") + generated_conf = self.config.parser.parsed[example_conf] + self.assertEqual( + [[['server'], [ + ['server_name', '.example.com'], + ['server_name', 'example.*'], [], + ['listen', '5001', 'ssl'], ['#', ' managed by Certbot'], + ['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed by Certbot'], + ['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by Certbot'], + ['include', self.config.mod_ssl_conf], ['#', ' managed by Certbot'], + ['ssl_dhparam', self.config.ssl_dhparams], ['#', ' managed by Certbot'], + [], [], + ['add_header', 'Strict-Transport-Security', '"max-age=31536000"', 'always'], + ['#', ' managed by Certbot'], + [], []]], + [['server'], [ + ['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], + ['server_name', '.example.com'], + ['server_name', 'example.*'], + [], [], []]]], + generated_conf) + + def test_http_header_hsts(self): + example_conf = self.config.parser.abs_path('sites-enabled/example.com') + self.config.enhance("www.example.com", "ensure-http-header", + "Strict-Transport-Security") + expected = ['add_header', 'Strict-Transport-Security', '"max-age=31536000"', 'always'] + generated_conf = self.config.parser.parsed[example_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") + self.assertRaises( + errors.PluginEnhancementAlreadyPresent, + self.config.enhance, "www.example.com", + "ensure-http-header", "Strict-Transport-Security") + + @mock.patch('certbot_nginx.obj.VirtualHost.contains_list') def test_certbot_redirect_exists(self, mock_contains_list): # Test that we add no redirect statement if there is already a diff --git a/certbot-nginx/certbot_nginx/tests/obj_test.py b/certbot-nginx/certbot_nginx/tests/obj_test.py index b30338b5b..9e5853c4a 100644 --- a/certbot-nginx/certbot_nginx/tests/obj_test.py +++ b/certbot-nginx/certbot_nginx/tests/obj_test.py @@ -14,6 +14,7 @@ class AddrTest(unittest.TestCase): self.addr5 = Addr.fromstring("myhost") self.addr6 = Addr.fromstring("80 default_server spdy") self.addr7 = Addr.fromstring("unix:/var/run/nginx.sock") + self.addr8 = Addr.fromstring("*:80 default ssl") def test_fromstring(self): self.assertEqual(self.addr1.get_addr(), "192.168.1.1") @@ -46,6 +47,8 @@ class AddrTest(unittest.TestCase): self.assertFalse(self.addr6.ssl) self.assertTrue(self.addr6.default) + self.assertTrue(self.addr8.default) + self.assertEqual(None, self.addr7) def test_str(self): @@ -55,6 +58,7 @@ class AddrTest(unittest.TestCase): self.assertEqual(str(self.addr4), "*:80 default_server ssl") self.assertEqual(str(self.addr5), "myhost") self.assertEqual(str(self.addr6), "80 default_server") + self.assertEqual(str(self.addr8), "*:80 default_server ssl") def test_to_string(self): self.assertEqual(self.addr1.to_string(), "192.168.1.1") @@ -77,7 +81,8 @@ class AddrTest(unittest.TestCase): from certbot_nginx.obj import Addr any_addresses = ("0.0.0.0:80 default_server ssl", "80 default_server ssl", - "*:80 default_server ssl") + "*:80 default_server ssl", + "80 default ssl") for first, second in itertools.combinations(any_addresses, 2): self.assertEqual(Addr.fromstring(first), Addr.fromstring(second)) @@ -143,6 +148,15 @@ class VirtualHostTest(unittest.TestCase): "filp", set([Addr.fromstring("localhost")]), False, False, set(['localhost']), raw4, []) + raw_has_hsts = [ + ['listen', '69.50.225.155:9000'], + ['server_name', 'return.com'], + ['add_header', 'always', 'set', 'Strict-Transport-Security', '\"max-age=31536000\"'], + ] + self.vhost_has_hsts = VirtualHost( + "filep", + set([Addr.fromstring("localhost")]), False, False, + set(['localhost']), raw_has_hsts, []) def test_eq(self): from certbot_nginx.obj import Addr @@ -162,6 +176,12 @@ class VirtualHostTest(unittest.TestCase): 'enabled: False']) self.assertEqual(stringified, str(self.vhost1)) + def test_has_header(self): + self.assertTrue(self.vhost_has_hsts.has_header('Strict-Transport-Security')) + self.assertFalse(self.vhost_has_hsts.has_header('Bogus-Header')) + self.assertFalse(self.vhost1.has_header('Strict-Transport-Security')) + self.assertFalse(self.vhost1.has_header('Bogus-Header')) + def test_contains_list(self): from certbot_nginx.obj import VirtualHost from certbot_nginx.obj import Addr diff --git a/certbot-nginx/certbot_nginx/tests/parser_test.py b/certbot-nginx/certbot_nginx/tests/parser_test.py index e21acb8ea..1e9703185 100644 --- a/certbot-nginx/certbot_nginx/tests/parser_test.py +++ b/certbot-nginx/certbot_nginx/tests/parser_test.py @@ -191,6 +191,31 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods ['server_name', '*.www.foo.com', '*.www.example.com']] self.assertTrue(nparser.has_ssl_on_directive(mock_vhost)) + + def test_remove_server_directives(self): + nparser = parser.NginxParser(self.config_path) + mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'), + None, None, None, + set(['localhost', + r'~^(www\.)?(example|bar)\.']), + None, [10, 1, 9]) + example_com = nparser.abs_path('sites-enabled/example.com') + names = set(['.example.com', 'example.*']) + mock_vhost.filep = example_com + mock_vhost.names = names + mock_vhost.path = [0] + nparser.add_server_directives(mock_vhost, + [['foo', 'bar'], ['ssl_certificate', + '/etc/ssl/cert2.pem']]) + nparser.remove_server_directives(mock_vhost, 'foo') + nparser.remove_server_directives(mock_vhost, 'ssl_certificate') + self.assertEqual(nparser.parsed[example_com], + [[['server'], [['listen', '69.50.225.155:9000'], + ['listen', '127.0.0.1'], + ['server_name', '.example.com'], + ['server_name', 'example.*'], + []]]]) + def test_add_server_directives(self): nparser = parser.NginxParser(self.config_path) mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'), @@ -200,8 +225,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods None, [10, 1, 9]) nparser.add_server_directives(mock_vhost, [['foo', 'bar'], ['\n ', 'ssl_certificate', ' ', - '/etc/ssl/cert.pem']], - replace=False) + '/etc/ssl/cert.pem']]) ssl_re = re.compile(r'\n\s+ssl_certificate /etc/ssl/cert.pem') dump = nginxparser.dumps(nparser.parsed[nparser.abs_path('nginx.conf')]) self.assertEqual(1, len(re.findall(ssl_re, dump))) @@ -213,10 +237,8 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods mock_vhost.path = [0] nparser.add_server_directives(mock_vhost, [['foo', 'bar'], ['ssl_certificate', - '/etc/ssl/cert2.pem']], - replace=False) - nparser.add_server_directives(mock_vhost, [['foo', 'bar']], - replace=False) + '/etc/ssl/cert2.pem']]) + nparser.add_server_directives(mock_vhost, [['foo', 'bar']]) from certbot_nginx.parser import COMMENT self.assertEqual(nparser.parsed[example_com], [[['server'], [['listen', '69.50.225.155:9000'], @@ -238,8 +260,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods nparser.add_server_directives, mock_vhost, [['foo', 'bar'], - ['ssl_certificate', '/etc/ssl/cert2.pem']], - replace=False) + ['ssl_certificate', '/etc/ssl/cert2.pem']]) def test_comment_is_repeatable(self): nparser = parser.NginxParser(self.config_path) @@ -249,12 +270,10 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods set(['.example.com', 'example.*']), None, [0]) nparser.add_server_directives(mock_vhost, - [['\n ', '#', ' ', 'what a nice comment']], - replace=False) + [['\n ', '#', ' ', 'what a nice comment']]) nparser.add_server_directives(mock_vhost, [['\n ', 'include', ' ', - nparser.abs_path('comment_in_file.conf')]], - replace=False) + nparser.abs_path('comment_in_file.conf')]]) from certbot_nginx.parser import COMMENT self.assertEqual(nparser.parsed[example_com], [[['server'], [['listen', '69.50.225.155:9000'], @@ -273,8 +292,8 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods target = set(['.example.com', 'example.*']) filep = nparser.abs_path('sites-enabled/example.com') mock_vhost = obj.VirtualHost(filep, None, None, None, target, None, [0]) - nparser.add_server_directives( - mock_vhost, [['server_name', 'foobar.com']], replace=True) + nparser.update_or_add_server_directives( + mock_vhost, [['server_name', 'foobar.com']]) from certbot_nginx.parser import COMMENT self.assertEqual( nparser.parsed[filep], @@ -284,8 +303,8 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods ['server_name', 'example.*'], [] ]]]) mock_vhost.names = set(['foobar.com', 'example.*']) - nparser.add_server_directives( - mock_vhost, [['ssl_certificate', 'cert.pem']], replace=True) + nparser.update_or_add_server_directives( + mock_vhost, [['ssl_certificate', 'cert.pem']]) self.assertEqual( nparser.parsed[filep], [[['server'], [['listen', '69.50.225.155:9000'], @@ -411,7 +430,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods vhosts = nparser.get_vhosts() default = [x for x in vhosts if 'default' in x.filep][0] - new_vhost = nparser.duplicate_vhost(default, delete_default=True) + new_vhost = nparser.duplicate_vhost(default, remove_singleton_listen_params=True) nparser.filedump(ext='') # check properties of new vhost @@ -429,6 +448,28 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods self.assertEqual(len(default.raw), len(new_vhost_parsed.raw)) self.assertTrue(next(iter(default.addrs)).super_eq(next(iter(new_vhost_parsed.addrs)))) + def test_duplicate_vhost_remove_ipv6only(self): + nparser = parser.NginxParser(self.config_path) + + vhosts = nparser.get_vhosts() + ipv6ssl = [x for x in vhosts if 'ipv6ssl' in x.filep][0] + new_vhost = nparser.duplicate_vhost(ipv6ssl, remove_singleton_listen_params=True) + nparser.filedump(ext='') + + for addr in new_vhost.addrs: + self.assertFalse(addr.ipv6only) + + identical_vhost = nparser.duplicate_vhost(ipv6ssl, remove_singleton_listen_params=False) + nparser.filedump(ext='') + + called = False + for addr in identical_vhost.addrs: + if addr.ipv6: + self.assertTrue(addr.ipv6only) + called = True + self.assertTrue(called) + + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index bb71cf19a..25023b307 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages -version = '0.22.0.dev0' +version = '0.23.0.dev0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. diff --git a/certbot-nginx/tests/boulder-integration.conf.sh b/certbot-nginx/tests/boulder-integration.conf.sh index c38180698..4374f9094 100755 --- a/certbot-nginx/tests/boulder-integration.conf.sh +++ b/certbot-nginx/tests/boulder-integration.conf.sh @@ -49,9 +49,9 @@ http { server { # IPv4. - listen 5002; + listen 5002 $default_server; # IPv6. - listen [::]:5002 default ipv6only=on; + listen [::]:5002 $default_server; server_name nginx.wtf nginx2.wtf; root $root/webroot; @@ -62,5 +62,36 @@ http { try_files \$uri \$uri/ /index.html; } } + + server { + listen 5002; + listen [::]:5002; + server_name nginx3.wtf; + + root $root/webroot; + + location /.well-known/ { + return 404; + } + + return 301 https://\$host\$request_uri; + } + + server { + listen 8082; + listen [::]:8082; + server_name nginx4.wtf nginx5.wtf; + } + + server { + listen 5002; + listen [::]:5002; + listen 5001 ssl; + listen [::]:5001 ssl; + if (\$scheme != "https") { + return 301 https://\$host\$request_uri; + } + server_name nginx6.wtf nginx7.wtf; + } } EOF diff --git a/certbot-nginx/tests/boulder-integration.sh b/certbot-nginx/tests/boulder-integration.sh index f236fb103..d6bd767ce 100755 --- a/certbot-nginx/tests/boulder-integration.sh +++ b/certbot-nginx/tests/boulder-integration.sh @@ -1,4 +1,4 @@ -#!/bin/sh -xe +#!/bin/bash -xe # prerequisite: apt-get install --no-install-recommends nginx-light openssl . ./tests/integration/_common.sh @@ -6,13 +6,15 @@ export PATH="/usr/sbin:$PATH" # /usr/sbin/nginx nginx_root="$root/nginx" mkdir $nginx_root -original=$(root="$nginx_root" ./certbot-nginx/tests/boulder-integration.conf.sh) -nginx_conf="$nginx_root/nginx.conf" -echo "$original" > $nginx_conf +reload_nginx () { + original=$(root="$nginx_root" ./certbot-nginx/tests/boulder-integration.conf.sh) + nginx_conf="$nginx_root/nginx.conf" + echo "$original" > $nginx_conf -killall nginx || true -nginx -c $nginx_root/nginx.conf + killall nginx || true + nginx -c $nginx_root/nginx.conf +} certbot_test_nginx () { certbot_test \ @@ -32,10 +34,30 @@ test_deployment_and_rollback() { diff -q <(echo "$original") $nginx_conf } +export default_server="default_server" +reload_nginx certbot_test_nginx --domains nginx.wtf run test_deployment_and_rollback nginx.wtf certbot_test_nginx --domains nginx2.wtf --preferred-challenges http test_deployment_and_rollback nginx2.wtf +# Overlapping location block and server-block-level return 301 +certbot_test_nginx --domains nginx3.wtf --preferred-challenges http +test_deployment_and_rollback nginx3.wtf +# No matching server block; default_server exists +certbot_test_nginx --domains nginx4.wtf --preferred-challenges http +test_deployment_and_rollback nginx4.wtf +# No matching server block; default_server does not exist +export default_server="" +reload_nginx +if nginx -c $nginx_root/nginx.conf -T 2>/dev/null | grep "default_server"; then + echo "Failed to remove default_server" + exit 1 +fi +certbot_test_nginx --domains nginx5.wtf --preferred-challenges http +test_deployment_and_rollback nginx5.wtf +# Mutiple domains, mix of matching and not +certbot_test_nginx --domains nginx6.wtf,nginx7.wtf --preferred-challenges http +test_deployment_and_rollback nginx6.wtf # note: not reached if anything above fails, hence "killall" at the # top diff --git a/certbot/__init__.py b/certbot/__init__.py index 2869d29b0..ebc8d5343 100644 --- a/certbot/__init__.py +++ b/certbot/__init__.py @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.22.0.dev0' +__version__ = '0.23.0.dev0' diff --git a/certbot/auth_handler.py b/certbot/auth_handler.py index 51cdf09ee..9d7c75f57 100644 --- a/certbot/auth_handler.py +++ b/certbot/auth_handler.py @@ -69,14 +69,15 @@ class AuthHandler(object): # While there are still challenges remaining... while self._has_challenges(aauthzrs): - resp = self._solve_challenges(aauthzrs) - logger.info("Waiting for verification...") - if config.debug_challenges: - notify('Challenges loaded. Press continue to submit to CA. ' - 'Pass "-v" for more info about challenges.', pause=True) + with error_handler.ExitHandler(self._cleanup_challenges, aauthzrs): + resp = self._solve_challenges(aauthzrs) + logger.info("Waiting for verification...") + if config.debug_challenges: + notify('Challenges loaded. Press continue to submit to CA. ' + 'Pass "-v" for more info about challenges.', pause=True) - # Send all Responses - this modifies achalls - self._respond(aauthzrs, resp, best_effort) + # Send all Responses - this modifies achalls + self._respond(aauthzrs, resp, best_effort) # Just make sure all decisions are complete. self.verify_authzr_complete(aauthzrs) @@ -118,14 +119,13 @@ class AuthHandler(object): """Get Responses for challenges from authenticators.""" resp = [] all_achalls = self._get_all_achalls(aauthzrs) - with error_handler.ErrorHandler(self._cleanup_challenges, all_achalls): - try: - if all_achalls: - resp = self.auth.perform(all_achalls) - except errors.AuthorizationError: - logger.critical("Failure in setting up challenges.") - logger.info("Attempting to clean up outstanding challenges...") - raise + try: + if all_achalls: + resp = self.auth.perform(all_achalls) + except errors.AuthorizationError: + logger.critical("Failure in setting up challenges.") + logger.info("Attempting to clean up outstanding challenges...") + raise assert len(resp) == len(all_achalls) @@ -147,13 +147,10 @@ class AuthHandler(object): """ # TODO: chall_update is a dirty hack to get around acme-spec #105 chall_update = dict() - active_achalls = self._send_responses(aauthzrs, resp, chall_update) + self._send_responses(aauthzrs, resp, chall_update) # Check for updated status... - try: - self._poll_challenges(aauthzrs, chall_update, best_effort) - finally: - self._cleanup_challenges(aauthzrs, active_achalls) + self._poll_challenges(aauthzrs, chall_update, best_effort) def _send_responses(self, aauthzrs, resps, chall_update): """Send responses and make sure errors are handled. @@ -161,6 +158,13 @@ class AuthHandler(object): :param aauthzrs: authorizations and the selected annotated challenges to try and perform :type aauthzrs: `list` of `AnnotatedAuthzr` + :param resps: challenge responses from the authenticator where + each response at index i corresponds to the annotated + challenge at index i in the list returned by + :func:`_get_all_achalls` + :type resps: `collections.abc.Iterable` of + :class:`~acme.challenges.ChallengeResponse` or `False` or + `None` :param dict chall_update: parameter that is updated to hold aauthzr index to list of outstanding solved annotated challenges @@ -287,19 +291,19 @@ class AuthHandler(object): chall_prefs.extend(plugin_pref) return chall_prefs - def _cleanup_challenges(self, aauthzrs, achall_list=None): + def _cleanup_challenges(self, aauthzrs, achalls=None): """Cleanup challenges. - If achall_list is not provided, cleanup all achallenges. + :param aauthzrs: authorizations and their selected annotated + challenges + :type aauthzrs: `list` of `AnnotatedAuthzr` + :param achalls: annotated challenges to cleanup + :type achalls: `list` of :class:`certbot.achallenges.AnnotatedChallenge` """ logger.info("Cleaning up challenges") - - if achall_list is None: + if achalls is None: achalls = self._get_all_achalls(aauthzrs) - else: - achalls = achall_list - if achalls: self.auth.cleanup(achalls) for achall in achalls: diff --git a/certbot/constants.py b/certbot/constants.py index a6878824b..9da5415d4 100644 --- a/certbot/constants.py +++ b/certbot/constants.py @@ -84,7 +84,7 @@ CLI_DEFAULTS = dict( config_dir="/etc/letsencrypt", work_dir="/var/lib/letsencrypt", logs_dir="/var/log/letsencrypt", - server="https://acme-v01.api.letsencrypt.org/directory", + server="https://acme-v02.api.letsencrypt.org/directory", # Plugins parsers configurator=None, @@ -107,7 +107,7 @@ CLI_DEFAULTS = dict( dns_route53=False ) -STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory" +STAGING_URI = "https://acme-staging-v02.api.letsencrypt.org/directory" # The set of reasons for revoking a certificate is defined in RFC 5280 in # section 5.3.1. The reasons that users are allowed to submit are restricted to @@ -135,13 +135,13 @@ RENEWER_DEFAULTS = dict( """Defaults for renewer script.""" -ENHANCEMENTS = ["redirect", "http-header", "ocsp-stapling", "spdy"] +ENHANCEMENTS = ["redirect", "ensure-http-header", "ocsp-stapling", "spdy"] """List of possible :class:`certbot.interfaces.IInstaller` enhancements. List of expected options parameters: - redirect: None -- http-header: TODO +- ensure-http-header: name of header (i.e. Strict-Transport-Security) - ocsp-stapling: certificate chain file path - spdy: TODO diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py index 37118c591..bd4e7fcfc 100644 --- a/certbot/crypto_util.py +++ b/certbot/crypto_util.py @@ -445,5 +445,5 @@ def cert_and_chain_from_fullchain(fullchain_pem): """ cert = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, fullchain_pem)).decode() - chain = fullchain_pem[len(cert):] + chain = fullchain_pem[len(cert):].lstrip() return (cert, chain) diff --git a/certbot/error_handler.py b/certbot/error_handler.py index 842243f70..e2737711e 100644 --- a/certbot/error_handler.py +++ b/certbot/error_handler.py @@ -24,7 +24,6 @@ if os.name != "nt": if signal.getsignal(signal_code) != signal.SIG_IGN: _SIGNALS.append(signal_code) - class ErrorHandler(object): """Context manager for running code that must be cleaned up on failure. @@ -55,6 +54,7 @@ class ErrorHandler(object): """ def __init__(self, func=None, *args, **kwargs): + self.call_on_regular_exit = False self.body_executed = False self.funcs = [] self.prev_handlers = {} @@ -70,8 +70,11 @@ class ErrorHandler(object): self.body_executed = True retval = False # SystemExit is ignored to properly handle forks that don't exec - if exec_type in (None, SystemExit): + if exec_type is SystemExit: return retval + elif exec_type is None: + if not self.call_on_regular_exit: + return retval elif exec_type is errors.SignalExit: logger.debug("Encountered signals: %s", self.received_signals) retval = True @@ -136,3 +139,15 @@ class ErrorHandler(object): for signum in self.received_signals: logger.debug("Calling signal %s", signum) os.kill(os.getpid(), signum) + +class ExitHandler(ErrorHandler): + """Context manager for running code that must be cleaned up. + + Subclass of ErrorHandler, with the same usage and parameters. + In addition to cleaning up on all signals, also cleans up on + regular exit. + """ + def __init__(self, func=None, *args, **kwargs): + ErrorHandler.__init__(self, func, *args, **kwargs) + self.call_on_regular_exit = True + diff --git a/certbot/log.py b/certbot/log.py index e0d2e8f11..face93cb3 100644 --- a/certbot/log.py +++ b/certbot/log.py @@ -19,7 +19,6 @@ import logging.handlers import os import sys import tempfile -import time import traceback from acme import messages @@ -148,7 +147,6 @@ def setup_log_file_handler(config, logfile, fmt): handler.doRollover() # TODO: creates empty letsencrypt.log.1 file handler.setLevel(logging.DEBUG) handler_formatter = logging.Formatter(fmt=fmt) - handler_formatter.converter = time.gmtime # don't use localtime handler.setFormatter(handler_formatter) return handler, log_file_path diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index 54e284d9e..9a8a13498 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -278,6 +278,43 @@ class HandleAuthorizationsTest(unittest.TestCase): self.assertRaises( errors.AuthorizationError, self.handler.handle_authorizations, mock_order) + def test_perform_error(self): + self.mock_auth.perform.side_effect = errors.AuthorizationError + + authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=True) + mock_order = mock.MagicMock(authorizations=[authzr]) + self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations, mock_order) + + self.assertEqual(self.mock_auth.cleanup.call_count, 1) + self.assertEqual( + self.mock_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01") + + @mock.patch("certbot.auth_handler.AuthHandler._respond") + def test_respond_error(self, mock_respond): + authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] + mock_order = mock.MagicMock(authorizations=authzrs) + mock_respond.side_effect = errors.AuthorizationError + + self.assertRaises( + errors.AuthorizationError, self.handler.handle_authorizations, mock_order) + self.assertEqual(self.mock_auth.cleanup.call_count, 1) + self.assertEqual( + self.mock_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01") + + @mock.patch("certbot.auth_handler.AuthHandler._poll_challenges") + @mock.patch("certbot.auth_handler.AuthHandler.verify_authzr_complete") + def test_incomplete_authzr_error(self, mock_verify, mock_poll): + authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] + mock_order = mock.MagicMock(authorizations=authzrs) + mock_verify.side_effect = errors.AuthorizationError + mock_poll.side_effect = self._validate_all + + self.assertRaises( + errors.AuthorizationError, self.handler.handle_authorizations, mock_order) + self.assertEqual(self.mock_auth.cleanup.call_count, 1) + self.assertEqual( + self.mock_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01") + def _validate_all(self, aauthzrs, unused_1, unused_2): for i, aauthzr in enumerate(aauthzrs): azr = aauthzr.authzr diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 480139378..2fe0e3d30 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -380,10 +380,12 @@ class CertAndChainFromFullchainTest(unittest.TestCase): cert_pem = CERT.decode() chain_pem = cert_pem + SS_CERT.decode() fullchain_pem = cert_pem + chain_pem + spacey_fullchain_pem = cert_pem + u'\n' + chain_pem from certbot.crypto_util import cert_and_chain_from_fullchain - cert_out, chain_out = cert_and_chain_from_fullchain(fullchain_pem) - self.assertEqual(cert_out, cert_pem) - self.assertEqual(chain_out, chain_pem) + for fullchain in (fullchain_pem, spacey_fullchain_pem): + cert_out, chain_out = cert_and_chain_from_fullchain(fullchain) + self.assertEqual(cert_out, cert_pem) + self.assertEqual(chain_out, chain_pem) if __name__ == '__main__': diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index 60dcf5e99..d4c48c242 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -36,7 +36,7 @@ def send_signal(signum): class ErrorHandlerTest(unittest.TestCase): - """Tests for certbot.error_handler.""" + """Tests for certbot.error_handler.ErrorHandler.""" def setUp(self): from certbot import error_handler @@ -47,6 +47,7 @@ class ErrorHandlerTest(unittest.TestCase): self.handler = error_handler.ErrorHandler(self.init_func, *self.init_args, **self.init_kwargs) + # pylint: disable=protected-access self.signals = error_handler._SIGNALS @@ -113,6 +114,33 @@ class ErrorHandlerTest(unittest.TestCase): pass self.assertFalse(self.init_func.called) + def test_regular_exit(self): + func = mock.MagicMock() + self.handler.register(func) + with self.handler: + pass + self.init_func.assert_not_called() + func.assert_not_called() + + +class ExitHandlerTest(ErrorHandlerTest): + """Tests for certbot.error_handler.ExitHandler.""" + + def setUp(self): + from certbot import error_handler + super(ExitHandlerTest, self).setUp() + self.handler = error_handler.ExitHandler(self.init_func, + *self.init_args, + **self.init_kwargs) + + def test_regular_exit(self): + func = mock.MagicMock() + self.handler.register(func) + with self.handler: + pass + self.init_func.assert_called_once_with(*self.init_args, + **self.init_kwargs) + func.assert_called_once_with() if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/tests/log_test.py b/certbot/tests/log_test.py index 3b0e1c5f6..549d2c5e1 100644 --- a/certbot/tests/log_test.py +++ b/certbot/tests/log_test.py @@ -156,7 +156,7 @@ class SetupLogFileHandlerTest(test_util.ConfigTestCase): handler.close() self.assertEqual(handler.level, logging.DEBUG) - self.assertEqual(handler.formatter.converter, time.gmtime) + self.assertEqual(handler.formatter.converter, time.localtime) expected_path = os.path.join(self.config.logs_dir, log_file) self.assertEqual(log_path, expected_path) diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index 6c0970e72..09c752ebe 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -726,7 +726,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.test_rc.configuration["renewalparams"] = {} rp = self.test_rc.configuration["renewalparams"] self.assertEqual(self.test_rc.is_test_cert, False) - rp["server"] = "https://acme-staging.api.letsencrypt.org/directory" + rp["server"] = "https://acme-staging-v02.api.letsencrypt.org/directory" self.assertEqual(self.test_rc.is_test_cert, True) rp["server"] = "https://staging.someotherca.com/directory" self.assertEqual(self.test_rc.is_test_cert, True) diff --git a/certbot/tests/testdata/sample-renewal.conf b/certbot/tests/testdata/sample-renewal.conf index 52b3ec45c..04f9ae8ca 100644 --- a/certbot/tests/testdata/sample-renewal.conf +++ b/certbot/tests/testdata/sample-renewal.conf @@ -61,7 +61,7 @@ chain_path = /home/ubuntu/letsencrypt/chain.pem break_my_certs = False standalone = True manual = False -server = https://acme-staging.api.letsencrypt.org/directory +server = https://acme-staging-v02.api.letsencrypt.org/directory standalone_supported_challenges = "tls-sni-01,http-01" webroot = False os_packages_only = False diff --git a/certbot/util.py b/certbot/util.py index f7ce6a3bc..b3973d96b 100644 --- a/certbot/util.py +++ b/certbot/util.py @@ -604,7 +604,7 @@ def enforce_domain_sanity(domain): def is_wildcard_domain(domain): """"Is domain a wildcard domain? - :param damain: domain to check + :param domain: domain to check :type domain: `bytes` or `str` or `unicode` :returns: True if domain is a wildcard, otherwise, False diff --git a/docs/cli-help.txt b/docs/cli-help.txt index abebdb9c9..399adc194 100644 --- a/docs/cli-help.txt +++ b/docs/cli-help.txt @@ -107,7 +107,7 @@ 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.21.1 (certbot; + "". (default: CertbotACMEClient/0.22.2 (certbot; darwin 10.13.3) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/2.7.14). The flags encoded in the user agent are: --duplicate, --force- @@ -200,7 +200,7 @@ testing: --test-cert, --staging Use the staging server to obtain or revoke test (invalid) certificates; equivalent to --server https - ://acme-staging.api.letsencrypt.org/directory + ://acme-staging-v02.api.letsencrypt.org/directory (default: False) --debug Show tracebacks in case of errors, and allow certbot- auto execution on experimental platforms (default: diff --git a/docs/contributing.rst b/docs/contributing.rst index 654528e3d..d8985b8d2 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -24,7 +24,7 @@ running: If you're on macOS, we recommend you skip the rest of this section and instead run Certbot in Docker. You can find instructions for how to do this :ref:`here -`. If you're running on Linux, you can run the following commands to +`. If you're running on Linux, you can run the following commands to install dependencies and set up a virtual environment where you can run Certbot. You will need to repeat this when Certbot's dependencies change or when a new plugin is introduced. @@ -43,7 +43,7 @@ each shell where you're working: .. code-block:: shell source ./venv/bin/activate - export SERVER=https://acme-staging.api.letsencrypt.org/directory + export SERVER=https://acme-staging-v02.api.letsencrypt.org/directory source tests/integration/_common.sh After that, your shell will be using the virtual environment, your copy of @@ -377,7 +377,7 @@ This should generate documentation in the ``docs/_build/html`` directory. -.. _docker: +.. _docker-dev: Running the client with Docker ============================== @@ -443,10 +443,10 @@ For squeeze you will need to: FreeBSD ------- -Packages can be installed on FreeBSD using ``pkg``, -or any other port-management tool (``portupgrade``, ``portmanager``, etc.) -from the pre-built package or can be built and installed from ports. -Either way will ensure proper installation of all the dependencies required +Packages can be installed on FreeBSD using ``pkg``, +or any other port-management tool (``portupgrade``, ``portmanager``, etc.) +from the pre-built package or can be built and installed from ports. +Either way will ensure proper installation of all the dependencies required for the package. FreeBSD by default uses ``tcsh``. In order to activate virtualenv (see diff --git a/docs/install.rst b/docs/install.rst index aec885b62..d47264545 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -94,6 +94,8 @@ Disable and remove the swapfile once the virtual environment is constructed:: user@webserver:~$ sudo swapoff /tmp/swapfile user@webserver:~$ sudo rm /tmp/swapfile +.. _docker-user: + Running with Docker ------------------- @@ -115,13 +117,17 @@ these make much sense to you, you should definitely use the certbot-auto_ method, which enables you to use installer plugins that cover both of those hard topics. -If you're still not convinced and have decided to use this method, -from the server that the domain you're requesting a certficate for resolves -to, `install Docker`_, then issue the following command: +If you're still not convinced and have decided to use this method, from +the server that the domain you're requesting a certficate for resolves +to, `install Docker`_, then issue a command like the one found below. If +you are using Certbot with the :ref:`Standalone` plugin, you will need +to make the port it uses accessible from outside of the container by +including something like ``-p 80:80`` or ``-p 443:443`` on the command +line before ``certbot/certbot``. .. code-block:: shell - sudo docker run -it --rm -p 443:443 -p 80:80 --name certbot \ + sudo docker run -it --rm --name certbot \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ certbot/certbot certonly @@ -131,6 +137,19 @@ Running Certbot with the ``certonly`` command will obtain a certificate and plac within Docker, you must install the certificate manually according to the procedure recommended by the provider of your webserver. +There are also Docker images for each of Certbot's DNS plugins available +at https://hub.docker.com/u/certbot which automate doing domain +validation over DNS for popular providers. To use one, just replace +``certbot/certbot`` in the command above with the name of the image you +want to use. For example, to use Certbot's plugin for Amazon Route 53, +you'd use ``certbot/dns-route53``. You may also need to add flags to +Certbot and/or mount additional directories to provide access to your +DNS API credentials as specified in the :ref:`DNS plugin documentation +`. If you would like to obtain a wildcard certificate from +Let's Encrypt's ACMEv2 server, you'll need to include ``--server +https://acme-v02.api.letsencrypt.org/directory`` on the command line as +well. + For more information about the layout of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`. @@ -187,10 +206,11 @@ want to use the Apache plugin, it has to be installed separately: emerge -av app-crypt/certbot emerge -av app-crypt/certbot-apache -When using the Apache plugin, you will run into a "cannot find a cert or key -directive" error if you're sporting the default Gentoo ``httpd.conf``. -You can fix this by commenting out two lines in ``/etc/apache2/httpd.conf`` -as follows: +When using the Apache plugin, you will run into a "cannot find an +SSLCertificateFile directive" or "cannot find an SSLCertificateKeyFile +directive for certificate" error if you're sporting the default Gentoo +``httpd.conf``. You can fix this by commenting out two lines in +``/etc/apache2/httpd.conf`` as follows: Change @@ -240,4 +260,3 @@ whole process is described in the :doc:`contributing`. e.g. ``sudo python setup.py install``, ``sudo pip install``, ``sudo ./venv/bin/...``. These modes of operation might corrupt your operating system and are **not supported** by the Certbot team! - diff --git a/docs/using.rst b/docs/using.rst index e8f84e2d7..7a25a5cc2 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -45,7 +45,7 @@ a combination_ of distinct authenticator and installer plugins. Plugin Auth Inst Notes Challenge types (and port) =========== ==== ==== =============================================================== ============================= apache_ Y Y | Automates obtaining and installing a certificate with Apache tls-sni-01_ (443) - | 2.4 on Debian-based distributions with ``libaugeas0`` 1.0+. + | 2.4 on OSes with ``libaugeas0`` 1.0+. webroot_ Y N | Obtains a certificate by writing to the webroot directory of http-01_ (80) | an already running webserver. nginx_ Y Y | Automates obtaining and installing a certificate with Nginx. tls-sni-01_ (443) @@ -54,12 +54,19 @@ standalone_ Y N | Uses a "standalone" webserver to obtain a certificate. | Requires port 80 or 443 to be available. This is useful on tls-sni-01_ (443) | systems with no webserver, or when direct integration with | the local webserver is not supported or not desired. +|dns_plugs| Y N | This category of plugins automates obtaining a certificate by dns-01_ (53) + | modifying DNS records to prove you have control over a + | domain. Doing domain validation in this way is + | the only way to obtain wildcard certificates from Let's + | Encrypt. manual_ Y N | Helps you obtain a certificate by giving you instructions to http-01_ (80), | perform domain validation yourself. Additionally allows you dns-01_ (53) or | to specify scripts to automate the validation task in a tls-sni-01_ (443) | customized way. =========== ==== ==== =============================================================== ============================= +.. |dns_plugs| replace:: :ref:`DNS plugins ` + Under the hood, plugins use one of several ACME protocol challenges_ to prove you control a domain. The options are http-01_ (which uses port 80), tls-sni-01_ (port 443) and dns-01_ (requiring configuration of a DNS server on @@ -80,7 +87,7 @@ Apache The Apache plugin currently requires an OS with augeas version 1.0; currently `it supports -`_ +`_ modern OSes based on Debian, Fedora, SUSE, Gentoo and Darwin. This automates both obtaining *and* installing certificates on an Apache webserver. To specify this plugin on the command line, simply include @@ -141,6 +148,8 @@ the ``--nginx`` flag on the commandline. certbot --nginx +.. _standalone: + Standalone ---------- @@ -164,6 +173,33 @@ the Internet on the specified port using each requested domain name. .. note:: The ``--standalone-supported-challenges`` option has been deprecated since ``certbot`` version 0.9.0. +.. _dns_plugins: + +DNS Plugins +----------- + +If you'd like to obtain a wildcard certificate from Let's Encrypt or run +``certbot`` on a machine other than your target webserver, you can use one of +Certbot's DNS plugins. + +These plugins are still in the process of being packaged +by many distributions and cannot currently be installed with ``certbot-auto``. +If, however, you are comfortable installing the certificates yourself, +you can run these plugins with :ref:`Docker `. + +Once installed, you can find documentation on how to use each plugin at: + +* `certbot-dns-cloudflare `_ +* `certbot-dns-cloudxns `_ +* `certbot-dns-digitalocean `_ +* `certbot-dns-dnsimple `_ +* `certbot-dns-dnsmadeeasy `_ +* `certbot-dns-google `_ +* `certbot-dns-luadns `_ +* `certbot-dns-nsone `_ +* `certbot-dns-rfc2136 `_ +* `certbot-dns-route53 `_ + Manual ------ @@ -516,6 +552,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 @@ -611,6 +653,31 @@ The following commands could be used to specify where these files are located:: sed -i 's,/etc/letsencrypt/live/example.com,/home/user/me/certbot,g' /etc/letsencrypt/renewal/example.com.conf certbot update_symlinks +Automated Renewals +------------------ + +Many Linux distributions provide automated renewal when you use the +packages installed through their system package manager. The +following table is an *incomplete* list of distributions which do so, +as well as their methods for doing so. + +If you are not sure whether or not your system has this already +automated, refer to your distribution's documentation, or check your +system's crontab (typically in `/etc/crontab/` and `/etc/cron.*/*` and +systemd timers (`systemctl list-timers`). + +.. csv-table:: Distributions with Automated Renewal + :header: "Distribution Name", "Distribution Version", "Automation Method" + + "CentOS", "EPEL 7", "systemd" + "Debian", "jessie", "cron, systemd" + "Debian", "stretch", "cron, systemd" + "Debian", "testing/sid", "cron, systemd" + "Fedora", "26", "systemd" + "Fedora", "27", "systemd" + "RHEL", "EPEL 7", "systemd" + "Ubuntu", "17.10", "cron, systemd" + "Ubuntu", "certbot PPA", "cron, systemd" .. _where-certs: @@ -801,6 +868,27 @@ Example usage for DNS-01 (Cloudflare API v4) (for example purposes only, do not .. _lock-files: +Changing the ACME Server +======================== + +By default, Certbot uses Let's Encrypt's initial production server at +https://acme-v01.api.letsencrypt.org/. You can tell Certbot to use a +different CA by providing ``--server`` on the command line or in a +:ref:`configuration file ` with the URL of the server's +ACME directory. For example, if you would like to use Let's Encrypt's +new ACMEv2 server, you would add ``--server +https://acme-v02.api.letsencrypt.org/directory`` to the command line. +Certbot will automatically select which version of the ACME protocol to +use based on the contents served at the provided URL. + +If you use ``--server`` to specify an ACME CA that implements a newer +version of the spec, you may be able to obtain a certificate for a +wildcard domain. Some CAs (such as Let's Encrypt) require that domain +validation for wildcard domains must be done through modifications to +DNS records which means that the dns-01_ challenge type must be used. To +see a list of Certbot plugins that support this challenge type and how +to use them, see plugins_. + Lock Files ========== @@ -831,7 +919,7 @@ Certbot accepts a global configuration file that applies its options to all invo of Certbot. Certificate specific configuration choices should be set in the ``.conf`` files that can be found in ``/etc/letsencrypt/renewal``. -By default no cli.ini file is created, after creating one +By default no cli.ini file is created, after creating one it is possible to specify the location of this configuration file with ``certbot-auto --config cli.ini`` (or shorter ``-c cli.ini``). An example configuration file is shown below: @@ -867,6 +955,12 @@ the oldest one to make room for new logs. The number of subsequent logs can be changed by passing the desired number to the command line flag ``--max-log-backups``. +.. note:: Some distributions, including Debian and Ubuntu, disable + certbot's internal log rotation in favor of a more traditional + logrotate script. If you are using a distribution's packages and + want to alter the log rotation, check `/etc/logrotate.d/` for a + certbot rotation script. + .. _command-line: Certbot command-line options diff --git a/examples/dev-cli.ini b/examples/dev-cli.ini index c02038ca1..a405a0aef 100644 --- a/examples/dev-cli.ini +++ b/examples/dev-cli.ini @@ -1,5 +1,5 @@ # Always use the staging/testing server - avoids rate limiting -server = https://acme-staging.api.letsencrypt.org/directory +server = https://acme-staging-v02.api.letsencrypt.org/directory # This is an example configuration file for developers config-dir = /tmp/le/conf diff --git a/letsencrypt-auto b/letsencrypt-auto index d3a5c23e5..8c9745a6f 100755 --- a/letsencrypt-auto +++ b/letsencrypt-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.21.1" +LE_AUTO_VERSION="0.22.2" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -47,6 +47,7 @@ Help for certbot itself cannot be provided until it is installed. --no-bootstrap do not install OS dependencies --no-self-upgrade do not download updates --os-packages-only install OS dependencies and exit + --install-only install certbot, upgrade if needed, and exit -v, --verbose provide more output -q, --quiet provide only update/error output; implies --non-interactive @@ -60,6 +61,8 @@ for arg in "$@" ; do DEBUG=1;; --os-packages-only) OS_PACKAGES_ONLY=1;; + --install-only) + INSTALL_ONLY=1;; --no-self-upgrade) # Do not upgrade this script (also prevents client upgrades, because each # copy of the script pins a hash of the python client) @@ -246,7 +249,7 @@ DeprecationBootstrap() { fi } -MIN_PYTHON_VERSION="2.6" +MIN_PYTHON_VERSION="2.7" MIN_PYVER=$(echo "$MIN_PYTHON_VERSION" | sed 's/\.//') # Sets LE_PYTHON to Python version string and PYVER to the first two # digits of the python version @@ -1196,24 +1199,24 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.21.1 \ - --hash=sha256:08f026078807fbcfd7bfab44c4d827ee287738fefcc86fbe1493ce752d2fdccb \ - --hash=sha256:e6c8e9b0b5e38834330831d5a91e1c08accdb9b4923855d14d524e7327e6c4ea -acme==0.21.1 \ - --hash=sha256:4b2b5ef80c755dfa30eb5c67ab4b4e66e7f205ad922b43170502c5f8d8ef1242 \ - --hash=sha256:296e8abf4f5a69af1a892416faceea90e15f39e2920bf87beeaad1d6ce70a60b -certbot-apache==0.21.1 \ - --hash=sha256:faa4af1033564a0e676d16940775593fb849527b494a15f6a816ad0ed4fa273c \ - --hash=sha256:0bce4419d4fdabbdda2223cff8db6794c5717632fb9511b00498ec00982a3fa5 -certbot-nginx==0.21.1 \ - --hash=sha256:3fad3b4722544558ce03132f853e18da5e516013086aaa40f1036aa6667c70a9 \ - --hash=sha256:55a32afe0950ff49d3118f93035463a46c85c2f399d261123f5fe973afdd4f64 +certbot==0.22.2 \ + --hash=sha256:c8c63bdf0fed6258bdbc892454314ec37bcd1c35a7f62524a083d93ccdfc420d \ + --hash=sha256:e6e3639293e78397f31f7d99e3c63aff82d91e2b0d50d146ee3c77f830464bef +acme==0.22.2 \ + --hash=sha256:59a55244612ee305d2caa6bb4cddd400fb60ec841bf011ed29a2899832a682c2 \ + --hash=sha256:0ecd0ea369f53d5bc744d6e72717f9af2e1ceb558d109dbd433148851027adb4 +certbot-apache==0.22.2 \ + --hash=sha256:b5340d4b9190358fde8eb6a5be0def37e32014b5142ee79ef5d2319ccbbde754 \ + --hash=sha256:3cd26912bb5732d917ddf7aad2fe870090d4ece9a408b2c2de8e9723ec99c759 +certbot-nginx==0.22.2 \ + --hash=sha256:91feef0d879496835d355e82841f92e5ecb5abbf6f23ea0ee5bbb8f5a92b278a \ + --hash=sha256:b10bf04c1a20cf878d5e0d1877deb0e0780bc31b0ffda08ce7199bbc39d0753b UNLIKELY_EOF # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py" #!/usr/bin/env python -"""A small script that can act as a trust root for installing pip 8 +"""A small script that can act as a trust root for installing pip >=8 Embed this in your project, and your VCS checkout is all you have to trust. In a post-peep era, this lets you claw your way to a hash-checking version of pip, @@ -1237,6 +1240,7 @@ anything goes wrong, it will exit with a non-zero status code. from __future__ import print_function from distutils.version import StrictVersion from hashlib import sha256 +from os import environ from os.path import join from pipes import quote from shutil import rmtree @@ -1270,14 +1274,14 @@ except ImportError: from urllib.parse import urlparse # 3.4 -__version__ = 1, 3, 0 +__version__ = 1, 5, 1 PIP_VERSION = '9.0.1' +DEFAULT_INDEX_BASE = 'https://pypi.python.org' # wheel has a conditional dependency on argparse: maybe_argparse = ( - [('https://pypi.python.org/packages/18/dd/' - 'e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/' + [('18/dd/e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/' 'argparse-1.4.0.tar.gz', '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')] if version_info < (2, 7, 0) else []) @@ -1285,18 +1289,14 @@ maybe_argparse = ( PACKAGES = maybe_argparse + [ # Pip has no dependencies, as it vendors everything: - ('https://pypi.python.org/packages/11/b6/' - 'abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/' - 'pip-{0}.tar.gz' - .format(PIP_VERSION), + ('11/b6/abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/' + 'pip-{0}.tar.gz'.format(PIP_VERSION), '09f243e1a7b461f654c26a725fa373211bb7ff17a9300058b205c61658ca940d'), # This version of setuptools has only optional dependencies: - ('https://pypi.python.org/packages/69/65/' - '4c544cde88d4d876cdf5cbc5f3f15d02646477756d89547e9a7ecd6afa76/' - 'setuptools-20.2.2.tar.gz', - '24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'), - ('https://pypi.python.org/packages/c9/1d/' - 'bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/' + ('59/88/2f3990916931a5de6fa9706d6d75eb32ee8b78627bb2abaab7ed9e6d0622/' + 'setuptools-29.0.1.tar.gz', + 'b539118819a4857378398891fa5366e090690e46b3e41421a1e07d6e9fd8feb0'), + ('c9/1d/bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/' 'wheel-0.29.0.tar.gz', '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648') ] @@ -1317,12 +1317,13 @@ def hashed_download(url, temp, digest): # >=2.7.9 verifies HTTPS certs itself, and, in any case, the cert # authenticity has only privacy (not arbitrary code execution) # implications, since we're checking hashes. - def opener(): + def opener(using_https=True): opener = build_opener(HTTPSHandler()) - # Strip out HTTPHandler to prevent MITM spoof: - for handler in opener.handlers: - if isinstance(handler, HTTPHandler): - opener.handlers.remove(handler) + if using_https: + # Strip out HTTPHandler to prevent MITM spoof: + for handler in opener.handlers: + if isinstance(handler, HTTPHandler): + opener.handlers.remove(handler) return opener def read_chunks(response, chunk_size): @@ -1332,8 +1333,9 @@ def hashed_download(url, temp, digest): break yield chunk - response = opener().open(url) - path = join(temp, urlparse(url).path.split('/')[-1]) + parsed_url = urlparse(url) + response = opener(using_https=parsed_url.scheme == 'https').open(url) + path = join(temp, parsed_url.path.split('/')[-1]) actual_hash = sha256() with open(path, 'wb') as file: for chunk in read_chunks(response, 4096): @@ -1346,6 +1348,24 @@ def hashed_download(url, temp, digest): return path +def get_index_base(): + """Return the URL to the dir containing the "packages" folder. + + Try to wring something out of PIP_INDEX_URL, if set. Hack "/simple" off the + end if it's there; that is likely to give us the right dir. + + """ + env_var = environ.get('PIP_INDEX_URL', '').rstrip('/') + if env_var: + SIMPLE = '/simple' + if env_var.endswith(SIMPLE): + return env_var[:-len(SIMPLE)] + else: + return env_var + else: + return DEFAULT_INDEX_BASE + + def main(): pip_version = StrictVersion(check_output(['pip', '--version']) .decode('utf-8').split()[1]) @@ -1353,11 +1373,13 @@ def main(): if pip_version >= min_pip_version: return 0 has_pip_cache = pip_version >= StrictVersion('6.0') - + index_base = get_index_base() temp = mkdtemp(prefix='pipstrap-') try: - downloads = [hashed_download(url, temp, digest) - for url, digest in PACKAGES] + downloads = [hashed_download(index_base + '/packages/' + path, + temp, + digest) + for path, digest in PACKAGES] check_output('pip install --no-index --no-deps -U ' + # Disable cache since we're not using it and it otherwise # sometimes throws permission warnings: @@ -1428,6 +1450,12 @@ UNLIKELY_EOF say "Installation succeeded." fi + + if [ "$INSTALL_ONLY" = 1 ]; then + say "Certbot is installed." + exit 0 + fi + "$VENV_BIN/letsencrypt" "$@" else diff --git a/letsencrypt-auto-source/Dockerfile.centos6 b/letsencrypt-auto-source/Dockerfile.centos6 index 47eb48f50..92fec168b 100644 --- a/letsencrypt-auto-source/Dockerfile.centos6 +++ b/letsencrypt-auto-source/Dockerfile.centos6 @@ -11,7 +11,7 @@ RUN yum install -y python-pip sudo COPY ./pieces/pipstrap.py /opt RUN /opt/pipstrap.py # Pin pytest version for increased stability -RUN pip install pytest==3.2.5 +RUN pip install pytest==3.2.5 six==1.10.0 # Add an unprivileged user: RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups wheel --uid 1000 lea diff --git a/letsencrypt-auto-source/Dockerfile.precise b/letsencrypt-auto-source/Dockerfile.precise index 71d572315..39a167c14 100644 --- a/letsencrypt-auto-source/Dockerfile.precise +++ b/letsencrypt-auto-source/Dockerfile.precise @@ -15,7 +15,7 @@ RUN apt-get update && \ COPY ./pieces/pipstrap.py /opt RUN /opt/pipstrap.py # Pin pytest version for increased stability -RUN pip install pytest==3.2.5 +RUN pip install pytest==3.2.5 six==1.10.0 # Let that user sudo: RUN sed -i.bkp -e \ diff --git a/letsencrypt-auto-source/Dockerfile.trusty b/letsencrypt-auto-source/Dockerfile.trusty index e0aacd118..3de88f9af 100644 --- a/letsencrypt-auto-source/Dockerfile.trusty +++ b/letsencrypt-auto-source/Dockerfile.trusty @@ -19,7 +19,7 @@ RUN apt-get update && \ COPY ./pieces/pipstrap.py /opt RUN /opt/pipstrap.py # Pin pytest version for increased stability -RUN pip install pytest==3.2.5 +RUN pip install pytest==3.2.5 six==1.10.0 RUN mkdir -p /home/lea/certbot diff --git a/letsencrypt-auto-source/Dockerfile.wheezy b/letsencrypt-auto-source/Dockerfile.wheezy index 56948d22a..f4f3fea15 100644 --- a/letsencrypt-auto-source/Dockerfile.wheezy +++ b/letsencrypt-auto-source/Dockerfile.wheezy @@ -14,7 +14,7 @@ RUN apt-get update && \ COPY ./pieces/pipstrap.py /opt RUN /opt/pipstrap.py # Pin pytest version for increased stability -RUN pip install pytest==3.2.5 +RUN pip install pytest==3.2.5 six==1.10.0 # Let that user sudo: RUN sed -i.bkp -e \ diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc index f28fd9893..3e1c4791c 100644 --- a/letsencrypt-auto-source/certbot-auto.asc +++ b/letsencrypt-auto-source/certbot-auto.asc @@ -1,11 +1,11 @@ -----BEGIN PGP SIGNATURE----- -iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAlpqMlYACgkQTRfJlc2X -dfKHfQgAnZQJ34jFoVqEodT0EjvkFKZif4V/zXTsVwTHn107BcLCpH/9gjANrSo3 -JpvseH2q0odhOAZA4rZKH4Geh+5fsUl3Ew9YB28RXeyqEfCATUqPq6q+jAi55SLc -a064Ux5N7eOIh9gxvpDKBeSFD0eNB8IDtPQhUspr+WnoycawrJHNGawL8WIfrWY3 -0ZPF981iPCWCdN3woDP9wHA2QtBClAk2pQ1aMgdkK9r/QLO+DY92xmT/Uu4ik2jR -zv+QplsQLftjD+bRar5R9jiCWV5phPqrOF3ypMiU0K5bsnrZfGBzBcoEyfKuB+UR -F/j/631OC6yLRasr+xcL1gc+SCryfA== -=tkZT +iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAlqwWJwACgkQTRfJlc2X +dfIzmwgAghmc3W63/qpCtJdezYeGLJdu03LvKoWYc7dTNYj2+0P5qmAAgCvKNY34 +qYzXA1jfCOgILSzRNE5WY+rbgjcmxxsxH+luYm6Ik0909MaMQ0D3h+5cRFs/tTtd +5cX0gxL3RQQTBwpnwbAZibe7lhjs9pXBiob2ek67hVr+xEwem69BQMlOhtYJbOs1 +osccoKc4NqaKbrfgOjjtMaL8YoRPO9vJHS9rRr6hxRZlPsmvusAHAiCbIrbX4XKE +CgxJFnuHK+amtfRoZg/xCqIK3Z94yZXPezywsri/YvDteOIs+DZ2qG/StfUrNYFX +WYfFFFyld0xwQtb4Oi9u4mx4sPg7lw== +=jZDE -----END PGP SIGNATURE----- diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index f97dc078d..07b313528 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-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.22.0.dev0" +LE_AUTO_VERSION="0.23.0.dev0" BASENAME=$(basename $0) USAGE="Usage: $BASENAME [OPTIONS] A self-updating wrapper script for the Certbot ACME client. When run, updates @@ -1199,18 +1199,18 @@ letsencrypt==0.7.0 \ --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \ --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9 -certbot==0.21.1 \ - --hash=sha256:08f026078807fbcfd7bfab44c4d827ee287738fefcc86fbe1493ce752d2fdccb \ - --hash=sha256:e6c8e9b0b5e38834330831d5a91e1c08accdb9b4923855d14d524e7327e6c4ea -acme==0.21.1 \ - --hash=sha256:4b2b5ef80c755dfa30eb5c67ab4b4e66e7f205ad922b43170502c5f8d8ef1242 \ - --hash=sha256:296e8abf4f5a69af1a892416faceea90e15f39e2920bf87beeaad1d6ce70a60b -certbot-apache==0.21.1 \ - --hash=sha256:faa4af1033564a0e676d16940775593fb849527b494a15f6a816ad0ed4fa273c \ - --hash=sha256:0bce4419d4fdabbdda2223cff8db6794c5717632fb9511b00498ec00982a3fa5 -certbot-nginx==0.21.1 \ - --hash=sha256:3fad3b4722544558ce03132f853e18da5e516013086aaa40f1036aa6667c70a9 \ - --hash=sha256:55a32afe0950ff49d3118f93035463a46c85c2f399d261123f5fe973afdd4f64 +certbot==0.22.2 \ + --hash=sha256:c8c63bdf0fed6258bdbc892454314ec37bcd1c35a7f62524a083d93ccdfc420d \ + --hash=sha256:e6e3639293e78397f31f7d99e3c63aff82d91e2b0d50d146ee3c77f830464bef +acme==0.22.2 \ + --hash=sha256:59a55244612ee305d2caa6bb4cddd400fb60ec841bf011ed29a2899832a682c2 \ + --hash=sha256:0ecd0ea369f53d5bc744d6e72717f9af2e1ceb558d109dbd433148851027adb4 +certbot-apache==0.22.2 \ + --hash=sha256:b5340d4b9190358fde8eb6a5be0def37e32014b5142ee79ef5d2319ccbbde754 \ + --hash=sha256:3cd26912bb5732d917ddf7aad2fe870090d4ece9a408b2c2de8e9723ec99c759 +certbot-nginx==0.22.2 \ + --hash=sha256:91feef0d879496835d355e82841f92e5ecb5abbf6f23ea0ee5bbb8f5a92b278a \ + --hash=sha256:b10bf04c1a20cf878d5e0d1877deb0e0780bc31b0ffda08ce7199bbc39d0753b UNLIKELY_EOF # ------------------------------------------------------------------------- diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index 8dd683775..088d5efa6 100644 Binary files a/letsencrypt-auto-source/letsencrypt-auto.sig and b/letsencrypt-auto-source/letsencrypt-auto.sig differ diff --git a/letsencrypt-auto-source/pieces/certbot-requirements.txt b/letsencrypt-auto-source/pieces/certbot-requirements.txt index 0a5994afc..865c3f6bc 100644 --- a/letsencrypt-auto-source/pieces/certbot-requirements.txt +++ b/letsencrypt-auto-source/pieces/certbot-requirements.txt @@ -1,12 +1,12 @@ -certbot==0.21.1 \ - --hash=sha256:08f026078807fbcfd7bfab44c4d827ee287738fefcc86fbe1493ce752d2fdccb \ - --hash=sha256:e6c8e9b0b5e38834330831d5a91e1c08accdb9b4923855d14d524e7327e6c4ea -acme==0.21.1 \ - --hash=sha256:4b2b5ef80c755dfa30eb5c67ab4b4e66e7f205ad922b43170502c5f8d8ef1242 \ - --hash=sha256:296e8abf4f5a69af1a892416faceea90e15f39e2920bf87beeaad1d6ce70a60b -certbot-apache==0.21.1 \ - --hash=sha256:faa4af1033564a0e676d16940775593fb849527b494a15f6a816ad0ed4fa273c \ - --hash=sha256:0bce4419d4fdabbdda2223cff8db6794c5717632fb9511b00498ec00982a3fa5 -certbot-nginx==0.21.1 \ - --hash=sha256:3fad3b4722544558ce03132f853e18da5e516013086aaa40f1036aa6667c70a9 \ - --hash=sha256:55a32afe0950ff49d3118f93035463a46c85c2f399d261123f5fe973afdd4f64 +certbot==0.22.2 \ + --hash=sha256:c8c63bdf0fed6258bdbc892454314ec37bcd1c35a7f62524a083d93ccdfc420d \ + --hash=sha256:e6e3639293e78397f31f7d99e3c63aff82d91e2b0d50d146ee3c77f830464bef +acme==0.22.2 \ + --hash=sha256:59a55244612ee305d2caa6bb4cddd400fb60ec841bf011ed29a2899832a682c2 \ + --hash=sha256:0ecd0ea369f53d5bc744d6e72717f9af2e1ceb558d109dbd433148851027adb4 +certbot-apache==0.22.2 \ + --hash=sha256:b5340d4b9190358fde8eb6a5be0def37e32014b5142ee79ef5d2319ccbbde754 \ + --hash=sha256:3cd26912bb5732d917ddf7aad2fe870090d4ece9a408b2c2de8e9723ec99c759 +certbot-nginx==0.22.2 \ + --hash=sha256:91feef0d879496835d355e82841f92e5ecb5abbf6f23ea0ee5bbb8f5a92b278a \ + --hash=sha256:b10bf04c1a20cf878d5e0d1877deb0e0780bc31b0ffda08ce7199bbc39d0753b diff --git a/letsencrypt-auto-source/tests/__init__.py b/letsencrypt-auto-source/tests/__init__.py index 45db90444..8a1613aa5 100644 --- a/letsencrypt-auto-source/tests/__init__.py +++ b/letsencrypt-auto-source/tests/__init__.py @@ -2,6 +2,6 @@ Run these locally by saying... :: - ./build.py && docker build -t lea . && docker run --rm -t -i lea + ./build.py && docker build -t lea . -f Dockerfile. && docker run --rm -t -i lea """ diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index d187452a1..c5109e208 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -18,6 +18,7 @@ from threading import Thread from unittest import TestCase from pytest import mark +from six.moves import xrange # pylint: disable=redefined-builtin @mark.skip diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt index 47440241d..d02204215 100644 --- a/tools/dev_constraints.txt +++ b/tools/dev_constraints.txt @@ -12,7 +12,7 @@ botocore==1.7.41 cloudflare==1.5.1 coverage==4.4.2 decorator==4.1.2 -dns-lexicon==2.1.14 +dns-lexicon==2.2.1 dnspython==1.15.0 docutils==0.14 execnet==1.5.0 diff --git a/tools/pip_install.sh b/tools/pip_install.sh index b385c5482..78e2afa17 100755 --- a/tools/pip_install.sh +++ b/tools/pip_install.sh @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/sh -e # pip installs packages using pinned package versions. If CERTBOT_OLDEST is set # to 1, a combination of tools/oldest_constraints.txt, # tools/dev_constraints.txt, and local-oldest-requirements.txt contained in the diff --git a/tools/simple_http_server.py b/tools/simple_http_server.py index 26bf231b7..14ac9a3d3 100755 --- a/tools/simple_http_server.py +++ b/tools/simple_http_server.py @@ -14,7 +14,7 @@ def serve_forever(port=0): """ server = HTTPServer(('', port), SimpleHTTPRequestHandler) - print 'Serving HTTP on {0} port {1} ...'.format(*server.server_address) + print('Serving HTTP on {0} port {1} ...'.format(*server.server_address)) sys.stdout.flush() server.serve_forever() diff --git a/tools/sphinx-quickstart.sh b/tools/sphinx-quickstart.sh index d67c45b6f..72dc9e200 100755 --- a/tools/sphinx-quickstart.sh +++ b/tools/sphinx-quickstart.sh @@ -25,7 +25,7 @@ API Documentation :glob: api/**" > api.rst -sed -i -e "s| :caption: Contents:| :caption: Contents:\n\n.. toctree::\n :maxdepth: 1\n\n api\n\n.. automodule:: ${PROJECT//-/_}\n :members:|" index.rst +sed -i -e "s| :caption: Contents:| :caption: Contents:\n\n.. automodule:: ${PROJECT//-/_}\n :members:\n\n.. toctree::\n :maxdepth: 1\n\n api|" index.rst echo "Suggested next steps: * Add API docs to: $PROJECT/docs/api/