Merge branch 'master' into warnings-are-errors

This commit is contained in:
Erica Portnoy
2018-10-30 17:18:00 -07:00
137 changed files with 2932 additions and 1566 deletions
+3 -1
View File
@@ -6,7 +6,8 @@ dist*/
/venv*/
/kgs/
/.tox/
/releases/
/releases*/
/log*
letsencrypt.log
certbot.log
letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64
@@ -39,6 +40,7 @@ tests/letstest/venv/
# pytest cache
.cache
.mypy_cache/
.pytest_cache/
# docker files
.docker
+5 -5
View File
@@ -13,11 +13,11 @@ before_script:
matrix:
include:
- python: "2.7"
env: TOXENV=py27_install BOULDER_INTEGRATION=v1
env: BOULDER_INTEGRATION=v1 INTEGRATION_TEST=all TOXENV=py27_install
sudo: required
services: docker
- python: "2.7"
env: TOXENV=py27_install BOULDER_INTEGRATION=v2
env: BOULDER_INTEGRATION=v2 INTEGRATION_TEST=all TOXENV=py27_install
sudo: required
services: docker
- python: "2.7"
@@ -34,7 +34,7 @@ matrix:
- python: "3.5"
env: TOXENV=mypy
- python: "2.7"
env: TOXENV='py27-{acme,apache,certbot,dns,nginx}-oldest'
env: TOXENV='py27-{acme,apache,certbot,dns,nginx,postfix}-oldest'
sudo: required
services: docker
- python: "3.4"
@@ -90,12 +90,12 @@ addons:
- nginx-light
- openssl
install: "travis_retry $(command -v pip || command -v pip3) install tox coveralls"
install: "travis_retry $(command -v pip || command -v pip3) install codecov tox"
script:
- travis_retry tox
- '[ -z "${BOULDER_INTEGRATION+x}" ] || (travis_retry tests/boulder-fetch.sh && tests/tox-boulder-integration.sh)'
after_success: '[ "$TOXENV" == "cover" ] && coveralls'
after_success: '[ "$TOXENV" == "cover" ] && codecov'
notifications:
email: false
+142 -1
View File
@@ -2,6 +2,147 @@
Certbot adheres to [Semantic Versioning](http://semver.org/).
## 0.28.0 - master
### Added
* `revoke` accepts `--cert-name`, and doesn't accept both `--cert-name` and `--cert-path`.
### Changed
* Removed documentation mentions of `#letsencrypt` IRC on Freenode.
* Write README to the base of (config-dir)/live directory
* `--manual` will explicitly warn users that earlier challenges should remain in place when setting up subsequent challenges.
### Fixed
* Match Nginx parser update in allowing variable names to start with `${`.
* Fix ranking of vhosts in Nginx so that all port-matching vhosts come first
* Correct OVH integration tests on machines without internet access.
* Stop caching the results of ipv6_info in http01.py
## 0.27.1 - 2018-09-06
### Fixed
* Fixed parameter name in OpenSUSE overrides for default parameters in the
Apache plugin. Certbot on OpenSUSE works again.
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
package with changes other than its version number was:
* certbot-apache
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/60?closed=1
## 0.27.0 - 2018-09-05
### Added
* The Apache plugin now accepts the parameter --apache-ctl which can be
used to configure the path to the Apache control script.
### Changed
* When using `acme.client.ClientV2` (or
`acme.client.BackwardsCompatibleClientV2` with an ACME server that supports a
newer version of the ACME protocol), an `acme.errors.ConflictError` will be
raised if you try to create an ACME account with a key that has already been
used. Previously, a JSON parsing error was raised in this scenario when using
the library with Let's Encrypt's ACMEv2 endpoint.
### Fixed
* When Apache is not installed, Certbot's Apache plugin no longer prints
messages about being unable to find apachectl to the terminal when the plugin
is not selected.
* If you're using the Apache plugin with the --apache-vhost-root flag set to a
directory containing a disabled virtual host for the domain you're requesting
a certificate for, the virtual host will now be temporarily enabled if
necessary to pass the HTTP challenge.
* The documentation for the Certbot package can now be built using Sphinx 1.6+.
* You can now call `query_registration` without having to first call
`new_account` on `acme.client.ClientV2` objects.
* The requirement of `setuptools>=1.0` has been removed from `certbot-dns-ovh`.
* Names in certbot-dns-sakuracloud's tests have been updated to refer to Sakura
Cloud rather than NS1 whose plugin certbot-dns-sakuracloud was based on.
Despite us having broken lockstep, we are continuing to release new versions of
all Certbot components during releases for the time being, however, the only
package with changes other than its version number was:
* acme
* certbot
* certbot-apache
* certbot-dns-ovh
* certbot-dns-sakuracloud
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/57?closed=1
## 0.26.1 - 2018-07-17
### Fixed
* Fix a bug that was triggered when users who had previously manually set `--server` to get ACMEv2 certs tried to renew ACMEv1 certs.
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 package with changes other than its version number was:
* certbot
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/58?closed=1
## 0.26.0 - 2018-07-11
### Added
* A new security enhancement which we're calling AutoHSTS has been added to
Certbot's Apache plugin. This enhancement configures your webserver to send a
HTTP Strict Transport Security header with a low max-age value that is slowly
increased over time. The max-age value is not increased to a large value
until you've successfully managed to renew your certificate. This enhancement
can be requested with the --auto-hsts flag.
* New official DNS plugins have been created for Gehirn Infrastracture Service,
Linode, OVH, and Sakura Cloud. These plugins can be found on our Docker Hub
page at https://hub.docker.com/u/certbot and on PyPI.
* The ability to reuse ACME accounts from Let's Encrypt's ACMEv1 endpoint on
Let's Encrypt's ACMEv2 endpoint has been added.
* Certbot and its components now support Python 3.7.
* Certbot's install subcommand now allows you to interactively choose which
certificate to install from the list of certificates managed by Certbot.
* Certbot now accepts the flag `--no-autorenew` which causes any obtained
certificates to not be automatically renewed when it approaches expiration.
* Support for parsing the TLS-ALPN-01 challenge has been added back to the acme
library.
### Changed
* Certbot's default ACME server has been changed to Let's Encrypt's ACMEv2
endpoint. By default, this server will now be used for both new certificate
lineages and renewals.
* The Nginx plugin is no longer marked labeled as an "Alpha" version.
* The `prepare` method of Certbot's plugins is no longer called before running
"Updater" enhancements that are run on every invocation of `certbot renew`.
Despite us having broken lockstep, we are continuing to release new versions of
all Certbot components during releases for the time being, however, the only
packages with functional changes were:
* acme
* certbot
* certbot-apache
* certbot-dns-gehirn
* certbot-dns-linode
* certbot-dns-ovh
* certbot-dns-sakuracloud
* certbot-nginx
More details about these changes can be found on our GitHub repo:
https://github.com/certbot/certbot/milestone/55?closed=1
## 0.25.1 - 2018-06-13
### Fixed
@@ -735,7 +876,7 @@ https://github.com/certbot/certbot/pulls?q=is%3Apr%20milestone%3A0.11.1%20is%3Ac
### Added
* When using the standalone plugin while running Certbot interactively
* When using the standalone plugin while running Certbot interactively
and a required port is bound by another process, Certbot will give you
the option to retry to grab the port rather than immediately exiting.
* You are now able to deactivate your account with the Let's Encrypt
-8
View File
@@ -1,8 +0,0 @@
ChangeLog
=========
To see the changes in a given release, view the issues closed in a given
release's GitHub milestone:
- `Past releases <https://github.com/certbot/certbot/milestones?state=closed>`_
- `Upcoming releases <https://github.com/certbot/certbot/milestones>`_
+1 -1
View File
@@ -5,7 +5,7 @@ EXPOSE 80 443
VOLUME /etc/letsencrypt /var/lib/letsencrypt
WORKDIR /opt/certbot
COPY CHANGES.rst README.rst setup.py src/
COPY CHANGELOG.md README.rst setup.py src/
COPY acme src/acme
COPY certbot src/certbot
+1 -1
View File
@@ -34,7 +34,7 @@ RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only
# Dockerfile we make sure we cache as much as possible
COPY setup.py README.rst CHANGES.rst MANIFEST.in letsencrypt-auto-source/pieces/pipstrap.py /opt/certbot/src/
COPY setup.py README.rst CHANGELOG.md MANIFEST.in letsencrypt-auto-source/pieces/pipstrap.py /opt/certbot/src/
# all above files are necessary for setup.py and venv setup, however,
# package source code directory has to be copied separately to a
+1 -1
View File
@@ -1,5 +1,5 @@
include README.rst
include CHANGES.rst
include CHANGELOG.md
include CONTRIBUTING.md
include LICENSE.txt
include linter_plugin.py
+3 -7
View File
@@ -6,7 +6,7 @@ Anyone who has gone through the trouble of setting up a secure website knows wha
How you use Certbot depends on the configuration of your web server. The best way to get started is to use our `interactive guide <https://certbot.eff.org>`_. It generates instructions based on your configuration settings. In most cases, youll need `root or administrator access <https://certbot.eff.org/faq/#does-certbot-require-root-administrator-privileges>`_ to your web server to run Certbot.
If youre using a hosted service and dont have direct access to your web server, you might not be able to use Certbot. Check with your hosting provider for documentation about uploading certificates or using certificates issued by Lets Encrypt.
Certbot is meant to be run directly on your web server, not on your personal computer. If youre using a hosted service and dont have direct access to your web server, you might not be able to use Certbot. Check with your hosting provider for documentation about uploading certificates or using certificates issued by Lets Encrypt.
Certbot is a fully-featured, extensible client for the Let's
Encrypt CA (or any other CA that speaks the `ACME
@@ -91,8 +91,6 @@ Main Website: https://certbot.eff.org
Let's Encrypt Website: https://letsencrypt.org
IRC Channel: #letsencrypt on `Freenode`_
Community: https://community.letsencrypt.org
ACME spec: http://ietf-wg-acme.github.io/acme/
@@ -101,14 +99,12 @@ ACME working area in github: https://github.com/ietf-wg-acme/acme
|build-status| |coverage| |docs| |container|
.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt
.. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master
:target: https://travis-ci.org/certbot/certbot
:alt: Travis CI status
.. |coverage| image:: https://coveralls.io/repos/certbot/certbot/badge.svg?branch=master
:target: https://coveralls.io/r/certbot/certbot
.. |coverage| image:: https://codecov.io/gh/certbot/certbot/branch/master/graph/badge.svg
:target: https://codecov.io/gh/certbot/certbot
:alt: Coverage status
.. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/
+17
View File
@@ -577,16 +577,33 @@ class ClientV2(ClientBase):
:param .NewRegistration new_account:
:raises .ConflictError: in case the account already exists
:returns: Registration Resource.
:rtype: `.RegistrationResource`
"""
response = self._post(self.directory['newAccount'], new_account)
# if account already exists
if response.status_code == 200 and 'Location' in response.headers:
raise errors.ConflictError(response.headers.get('Location'))
# "Instance of 'Field' has no key/contact member" bug:
# pylint: disable=no-member
regr = self._regr_from_response(response)
self.net.account = regr
return regr
def query_registration(self, regr):
"""Query server about registration.
:param messages.RegistrationResource: Existing Registration
Resource.
"""
self.net.account = regr
updated_regr = super(ClientV2, self).query_registration(regr)
self.net.account = updated_regr
return updated_regr
def update_registration(self, regr, update=None):
"""Update registration.
+13 -2
View File
@@ -134,6 +134,12 @@ class BackwardsCompatibleClientV2Test(ClientTestBase):
client = self._init()
self.assertEqual(client.acme_version, 2)
def test_query_registration_client_v2(self):
self.response.json.return_value = DIRECTORY_V2.to_json()
client = self._init()
self.response.json.return_value = self.regr.body.to_json()
self.assertEqual(self.regr, client.query_registration(self.regr))
def test_forwarding(self):
self.response.json.return_value = DIRECTORY_V1.to_json()
client = self._init()
@@ -706,6 +712,11 @@ class ClientV2Test(ClientTestBase):
self.assertEqual(self.regr, self.client.new_account(self.new_reg))
def test_new_account_conflict(self):
self.response.status_code = http_client.OK
self.response.headers['Location'] = self.regr.uri
self.assertRaises(errors.ConflictError, self.client.new_account, self.new_reg)
def test_new_order(self):
order_response = copy.deepcopy(self.response)
order_response.status_code = http_client.CREATED
@@ -1027,8 +1038,8 @@ class ClientNetworkTest(unittest.TestCase):
# Requests Library Exceptions
except requests.exceptions.ConnectionError as z: #pragma: no cover
self.assertEqual("('Connection aborted.', "
"error(111, 'Connection refused'))", str(z))
self.assertTrue("('Connection aborted.', error(111, 'Connection refused'))"
== str(z) or "[WinError 10061]" in str(z))
class ClientNetworkWithMockedResponseTest(unittest.TestCase):
"""Tests for acme.client.ClientNetwork which mock out response."""
+2 -8
View File
@@ -136,22 +136,16 @@ def probe_sni(name, host, port=443, timeout=300,
socket_kwargs = {'source_address': source_address}
host_protocol_agnostic = host
if host == '::' or host == '0':
# https://github.com/python/typeshed/pull/2136
# while PR is not merged, we need to ignore
host_protocol_agnostic = None
try:
# pylint: disable=star-args
logger.debug(
"Attempting to connect to %s:%d%s.", host_protocol_agnostic, port,
"Attempting to connect to %s:%d%s.", host, port,
" from {0}:{1}".format(
source_address[0],
source_address[1]
) if socket_kwargs else ""
)
socket_tuple = (host_protocol_agnostic, port) # type: Tuple[Optional[str], int]
socket_tuple = (host, port) # type: Tuple[str, int]
sock = socket.create_connection(socket_tuple, **socket_kwargs) # type: ignore
except socket.error as error:
raise errors.Error(error)
+2
View File
@@ -110,6 +110,8 @@ class ConflictError(ClientError):
In the version of ACME implemented by Boulder, this is used to find an
account if you only have the private key, but don't know the account URL.
Also used in V2 of the ACME client for the same purpose.
"""
def __init__(self, location):
self.location = location
+1 -2
View File
@@ -523,7 +523,7 @@ class Order(ResourceBody):
"""
identifiers = jose.Field('identifiers', omitempty=True)
status = jose.Field('status', decoder=Status.from_json,
omitempty=True, default=STATUS_PENDING)
omitempty=True)
authorizations = jose.Field('authorizations', omitempty=True)
certificate = jose.Field('certificate', omitempty=True)
finalize = jose.Field('finalize', omitempty=True)
@@ -553,4 +553,3 @@ class OrderResource(ResourceWithURI):
class NewOrder(Order):
"""New order."""
resource_type = 'new-order'
resource = fields.Resource(resource_type)
+13
View File
@@ -424,6 +424,19 @@ class OrderResourceTest(unittest.TestCase):
'authorizations': None,
})
class NewOrderTest(unittest.TestCase):
"""Tests for acme.messages.NewOrder."""
def setUp(self):
from acme.messages import NewOrder
self.reg = NewOrder(
identifiers=mock.sentinel.identifiers)
def test_to_partial_json(self):
self.assertEqual(self.reg.to_json(), {
'identifiers': mock.sentinel.identifiers,
})
if __name__ == '__main__':
unittest.main() # pragma: no cover
+11 -7
View File
@@ -48,7 +48,7 @@ class TLSSNI01ServerTest(unittest.TestCase):
test_util.load_cert('rsa2048_cert.pem'),
)}
from acme.standalone import TLSSNI01Server
self.server = TLSSNI01Server(("", 0), certs=self.certs)
self.server = TLSSNI01Server(('localhost', 0), certs=self.certs)
# pylint: disable=no-member
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.start()
@@ -133,8 +133,11 @@ class BaseDualNetworkedServersTest(unittest.TestCase):
self.address_family = socket.AF_INET
socketserver.TCPServer.__init__(self, *args, **kwargs)
if ipv6:
# NB: On Windows, socket.IPPROTO_IPV6 constant may be missing.
# We use the corresponding value (41) instead.
level = getattr(socket, "IPPROTO_IPV6", 41)
# pylint: disable=no-member
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
self.socket.setsockopt(level, socket.IPV6_V6ONLY, 1)
try:
self.server_bind()
self.server_activate()
@@ -147,15 +150,15 @@ class BaseDualNetworkedServersTest(unittest.TestCase):
mock_bind.side_effect = socket.error
from acme.standalone import BaseDualNetworkedServers
self.assertRaises(socket.error, BaseDualNetworkedServers,
BaseDualNetworkedServersTest.SingleProtocolServer,
("", 0),
socketserver.BaseRequestHandler)
BaseDualNetworkedServersTest.SingleProtocolServer,
('', 0),
socketserver.BaseRequestHandler)
def test_ports_equal(self):
from acme.standalone import BaseDualNetworkedServers
servers = BaseDualNetworkedServers(
BaseDualNetworkedServersTest.SingleProtocolServer,
("", 0),
('', 0),
socketserver.BaseRequestHandler)
socknames = servers.getsocknames()
prev_port = None
@@ -177,7 +180,7 @@ class TLSSNI01DualNetworkedServersTest(unittest.TestCase):
test_util.load_cert('rsa2048_cert.pem'),
)}
from acme.standalone import TLSSNI01DualNetworkedServers
self.servers = TLSSNI01DualNetworkedServers(("", 0), certs=self.certs)
self.servers = TLSSNI01DualNetworkedServers(('localhost', 0), certs=self.certs)
self.servers.serve_forever()
def tearDown(self):
@@ -245,6 +248,7 @@ class HTTP01DualNetworkedServersTest(unittest.TestCase):
self.assertFalse(self._test_http01(add=False))
@test_util.broken_on_windows
class TestSimpleTLSSNI01Server(unittest.TestCase):
"""Tests for acme.standalone.simple_tls_sni_01_server."""
+9
View File
@@ -4,6 +4,7 @@
"""
import os
import sys
import pkg_resources
import unittest
@@ -94,3 +95,11 @@ def skip_unless(condition, reason): # pragma: no cover
return lambda cls: cls
else:
return lambda cls: None
def broken_on_windows(function):
"""Decorator to skip temporarily a broken test on Windows."""
reason = 'Test is broken and ignored on windows but should be fixed.'
return unittest.skipIf(
sys.platform == 'win32'
and os.environ.get('SKIP_BROKEN_TESTS_ON_WINDOWS', 'true') == 'true',
reason)(function)
+1 -1
View File
@@ -3,7 +3,7 @@ from setuptools import find_packages
from setuptools.command.test import test as TestCommand
import sys
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
+29
View File
@@ -0,0 +1,29 @@
image:
# => Windows Server 2012 R2
- Visual Studio 2015
# => Windows Server 2016
- Visual Studio 2017
branches:
only:
- master
- /^\d+\.\d+\.x$/ # Version branches like X.X.X
- /^test-.*$/
install:
# Use Python 3.7 by default
- "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%"
# Check env
- "python --version"
# Upgrade pip to avoid warnings
- "python -m pip install --upgrade pip"
# Ready to install tox and coverage
- "pip install tox codecov"
build: off
test_script:
- tox -c tox-win.ini -e py34,py35,py36,py37-cover
on_success:
- codecov
+69 -46
View File
@@ -1,5 +1,6 @@
"""Apache Configuration based off of Augeas Configurator."""
# pylint: disable=too-many-lines
import copy
import fnmatch
import logging
import os
@@ -97,48 +98,72 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
vhost_root="/etc/apache2/sites-available",
vhost_files="*",
logs_root="/var/log/apache2",
ctl="apache2ctl",
version_cmd=['apache2ctl', '-v'],
apache_cmd="apache2ctl",
restart_cmd=['apache2ctl', 'graceful'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_mods=False,
handle_modules=False,
handle_sites=False,
challenge_location="/etc/apache2",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "options-ssl-apache.conf")
)
def constant(self, key):
"""Get constant for OS_DEFAULTS"""
return self.OS_DEFAULTS.get(key)
def option(self, key):
"""Get a value from options"""
return self.options.get(key)
def _prepare_options(self):
"""
Set the values possibly changed by command line parameters to
OS_DEFAULTS constant dictionary
"""
opts = ["enmod", "dismod", "le_vhost_ext", "server_root", "vhost_root",
"logs_root", "challenge_location", "handle_modules", "handle_sites",
"ctl"]
for o in opts:
# Config options use dashes instead of underscores
if self.conf(o.replace("_", "-")) is not None:
self.options[o] = self.conf(o.replace("_", "-"))
else:
self.options[o] = self.OS_DEFAULTS[o]
# Special cases
self.options["version_cmd"][0] = self.option("ctl")
self.options["restart_cmd"][0] = self.option("ctl")
self.options["conftest_cmd"][0] = self.option("ctl")
@classmethod
def add_parser_arguments(cls, add):
# When adding, modifying or deleting command line arguments, be sure to
# include the changes in the list used in method _prepare_options() to
# ensure consistent behavior.
add("enmod", default=cls.OS_DEFAULTS["enmod"],
help="Path to the Apache 'a2enmod' binary.")
help="Path to the Apache 'a2enmod' binary")
add("dismod", default=cls.OS_DEFAULTS["dismod"],
help="Path to the Apache 'a2dismod' binary.")
help="Path to the Apache 'a2dismod' binary")
add("le-vhost-ext", default=cls.OS_DEFAULTS["le_vhost_ext"],
help="SSL vhost configuration extension.")
help="SSL vhost configuration extension")
add("server-root", default=cls.OS_DEFAULTS["server_root"],
help="Apache server root directory.")
help="Apache server root directory")
add("vhost-root", default=None,
help="Apache server VirtualHost configuration root")
add("logs-root", default=cls.OS_DEFAULTS["logs_root"],
help="Apache server logs directory")
add("challenge-location",
default=cls.OS_DEFAULTS["challenge_location"],
help="Directory path for challenge configuration.")
add("handle-modules", default=cls.OS_DEFAULTS["handle_mods"],
help="Let installer handle enabling required modules for you. " +
help="Directory path for challenge configuration")
add("handle-modules", default=cls.OS_DEFAULTS["handle_modules"],
help="Let installer handle enabling required modules for you " +
"(Only Ubuntu/Debian currently)")
add("handle-sites", default=cls.OS_DEFAULTS["handle_sites"],
help="Let installer handle enabling sites for you. " +
help="Let installer handle enabling sites for you " +
"(Only Ubuntu/Debian currently)")
util.add_deprecated_argument(add, argument_name="ctl", nargs=1)
add("ctl", default=cls.OS_DEFAULTS["ctl"],
help="Full path to Apache control script")
util.add_deprecated_argument(
add, argument_name="init-script", nargs=1)
@@ -169,7 +194,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.parser = None
self.version = version
self.vhosts = None
self.vhostroot = None
self.options = copy.deepcopy(self.OS_DEFAULTS)
self._enhance_func = {"redirect": self._enable_redirect,
"ensure-http-header": self._set_http_header,
"staple-ocsp": self._enable_ocsp_stapling}
@@ -201,12 +226,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
except ImportError:
raise errors.NoInstallationError("Problem in Augeas installation")
self._prepare_options()
# Verify Apache is installed
restart_cmd = self.constant("restart_cmd")[0]
if not util.exe_exists(restart_cmd):
if not path_surgery(restart_cmd):
raise errors.NoInstallationError(
'Cannot find Apache control command {0}'.format(restart_cmd))
self._verify_exe_availability(self.option("ctl"))
# Make sure configuration is valid
self.config_test()
@@ -226,12 +249,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"version 1.2.0 or higher, please make sure you have you have "
"those installed.")
# Parse vhost-root if defined on cli
if not self.conf("vhost-root"):
self.vhostroot = self.constant("vhost_root")
else:
self.vhostroot = os.path.abspath(self.conf("vhost-root"))
self.parser = self.get_parser()
# Check for errors in parsing files with Augeas
@@ -245,13 +262,20 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Prevent two Apache plugins from modifying a config at once
try:
util.lock_dir_until_exit(self.conf("server-root"))
util.lock_dir_until_exit(self.option("server_root"))
except (OSError, errors.LockError):
logger.debug("Encountered error:", exc_info=True)
raise errors.PluginError(
"Unable to lock %s", self.conf("server-root"))
"Unable to lock %s", self.option("server_root"))
self._prepared = True
def _verify_exe_availability(self, exe):
"""Checks availability of Apache executable"""
if not util.exe_exists(exe):
if not path_surgery(exe):
raise errors.NoInstallationError(
'Cannot find Apache executable {0}'.format(exe))
def _check_aug_version(self):
""" Checks that we have recent enough version of libaugeas.
If augeas version is recent enough, it will support case insensitive
@@ -269,8 +293,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
def get_parser(self):
"""Initializes the ApacheParser"""
# If user provided vhost_root value in command line, use it
return parser.ApacheParser(
self.aug, self.conf("server-root"), self.conf("vhost-root"),
self.aug, self.option("server_root"), self.conf("vhost-root"),
self.version, configurator=self)
def _wildcard_domain(self, domain):
@@ -1037,7 +1062,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param boolean temp: If the change is temporary
"""
if self.conf("handle-modules"):
if self.option("handle_modules"):
if self.version >= (2, 4) and ("socache_shmcb_module" not in
self.parser.modules):
self.enable_mod("socache_shmcb", temp=temp)
@@ -1066,7 +1091,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
Duplicates vhost and adds default ssl options
New vhost will reside as (nonssl_vhost.path) +
``self.constant("le_vhost_ext")``
``self.option("le_vhost_ext")``
.. note:: This function saves the configuration
@@ -1165,18 +1190,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
if self.conf("vhost-root") and os.path.exists(self.conf("vhost-root")):
# Defined by user on CLI
fp = os.path.join(os.path.realpath(self.vhostroot),
fp = os.path.join(os.path.realpath(self.option("vhost_root")),
os.path.basename(non_ssl_vh_fp))
else:
# Use non-ssl filepath
fp = os.path.realpath(non_ssl_vh_fp)
if fp.endswith(".conf"):
return fp[:-(len(".conf"))] + self.conf("le_vhost_ext")
return fp[:-(len(".conf"))] + self.option("le_vhost_ext")
else:
return fp + self.conf("le_vhost_ext")
return fp + self.option("le_vhost_ext")
def _sift_rewrite_rule(self, line):
"""Decides whether a line should be copied to a SSL vhost.
@@ -2025,7 +2048,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
addr in self._get_proposed_addrs(ssl_vhost)),
servername, serveralias,
" ".join(rewrite_rule_args),
self.conf("logs-root")))
self.option("logs_root")))
def _write_out_redirect(self, ssl_vhost, text):
# This is the default name
@@ -2037,7 +2060,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if len(ssl_vhost.name) < (255 - (len(redirect_filename) + 1)):
redirect_filename = "le-redirect-%s.conf" % ssl_vhost.name
redirect_filepath = os.path.join(self.vhostroot,
redirect_filepath = os.path.join(self.option("vhost_root"),
redirect_filename)
# Register the new file that will be created
@@ -2158,18 +2181,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
error = ""
try:
util.run_script(self.constant("restart_cmd"))
util.run_script(self.option("restart_cmd"))
except errors.SubprocessError as err:
logger.info("Unable to restart apache using %s",
self.constant("restart_cmd"))
alt_restart = self.constant("restart_cmd_alt")
self.option("restart_cmd"))
alt_restart = self.option("restart_cmd_alt")
if alt_restart:
logger.debug("Trying alternative restart command: %s",
alt_restart)
# There is an alternative restart command available
# This usually is "restart" verb while original is "graceful"
try:
util.run_script(self.constant(
util.run_script(self.option(
"restart_cmd_alt"))
return
except errors.SubprocessError as secerr:
@@ -2185,7 +2208,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
try:
util.run_script(self.constant("conftest_cmd"))
util.run_script(self.option("conftest_cmd"))
except errors.SubprocessError as err:
raise errors.MisconfigurationError(str(err))
@@ -2201,11 +2224,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
try:
stdout, _ = util.run_script(self.constant("version_cmd"))
stdout, _ = util.run_script(self.option("version_cmd"))
except errors.SubprocessError:
raise errors.PluginError(
"Unable to run %s -v" %
self.constant("version_cmd"))
self.option("version_cmd"))
regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
matches = regex.findall(stdout)
@@ -2295,7 +2318,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# certbot for unprivileged users via setuid), this function will need
# 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)
self.option("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES)
def enable_autohsts(self, _unused_lineage, domains):
"""
+1 -2
View File
@@ -113,8 +113,7 @@ def _vhost_menu(domain, vhosts):
code, tag = zope.component.getUtility(interfaces.IDisplay).menu(
"We were unable to find a vhost with a ServerName "
"or Address of {0}.{1}Which virtual host would you "
"like to choose?\n(note: conf files with multiple "
"vhosts are not yet supported)".format(domain, os.linesep),
"like to choose?".format(domain, os.linesep),
choices, force_interactive=True)
except errors.MissingCommandlineFlag:
msg = (
+6
View File
@@ -6,6 +6,7 @@ from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-m
from certbot import errors
from certbot.plugins import common
from certbot_apache.obj import VirtualHost # pylint: disable=unused-import
from certbot_apache.parser import get_aug_path
logger = logging.getLogger(__name__)
@@ -172,4 +173,9 @@ class ApacheHttp01(common.TLSSNI01):
self.configurator.parser.add_dir(
vhost.path, "Include", self.challenge_conf_post)
if not vhost.enabled:
self.configurator.parser.add_dir(
get_aug_path(self.configurator.parser.loc["default"]),
"Include", vhost.filep)
self.moded_vhosts.add(vhost)
@@ -16,14 +16,14 @@ class ArchConfigurator(configurator.ApacheConfigurator):
vhost_root="/etc/httpd/conf",
vhost_files="*.conf",
logs_root="/var/log/httpd",
ctl="apachectl",
version_cmd=['apachectl', '-v'],
apache_cmd="apachectl",
restart_cmd=['apachectl', 'graceful'],
conftest_cmd=['apachectl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_mods=False,
handle_modules=False,
handle_sites=False,
challenge_location="/etc/httpd/conf",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
@@ -18,25 +18,33 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
vhost_root="/etc/httpd/conf.d",
vhost_files="*.conf",
logs_root="/var/log/httpd",
ctl="apachectl",
version_cmd=['apachectl', '-v'],
apache_cmd="apachectl",
restart_cmd=['apachectl', 'graceful'],
restart_cmd_alt=['apachectl', 'restart'],
conftest_cmd=['apachectl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_mods=False,
handle_modules=False,
handle_sites=False,
challenge_location="/etc/httpd/conf.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "centos-options-ssl-apache.conf")
)
def _prepare_options(self):
"""
Override the options dictionary initialization in order to support
alternative restart cmd used in CentOS.
"""
super(CentOSConfigurator, self)._prepare_options()
self.options["restart_cmd_alt"][0] = self.option("ctl")
def get_parser(self):
"""Initializes the ApacheParser"""
return CentOSParser(
self.aug, self.conf("server-root"), self.conf("vhost-root"),
self.aug, self.option("server_root"), self.option("vhost_root"),
self.version, configurator=self)
@@ -16,14 +16,14 @@ class DarwinConfigurator(configurator.ApacheConfigurator):
vhost_root="/etc/apache2/other",
vhost_files="*.conf",
logs_root="/var/log/apache2",
version_cmd=['/usr/sbin/httpd', '-v'],
apache_cmd="/usr/sbin/httpd",
ctl="apachectl",
version_cmd=['apachectl', '-v'],
restart_cmd=['apachectl', 'graceful'],
conftest_cmd=['apachectl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_mods=False,
handle_modules=False,
handle_sites=False,
challenge_location="/etc/apache2/other",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
@@ -23,14 +23,14 @@ class DebianConfigurator(configurator.ApacheConfigurator):
vhost_root="/etc/apache2/sites-available",
vhost_files="*",
logs_root="/var/log/apache2",
ctl="apache2ctl",
version_cmd=['apache2ctl', '-v'],
apache_cmd="apache2ctl",
restart_cmd=['apache2ctl', 'graceful'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod="a2enmod",
dismod="a2dismod",
le_vhost_ext="-le-ssl.conf",
handle_mods=True,
handle_modules=True,
handle_sites=True,
challenge_location="/etc/apache2",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
@@ -134,11 +134,11 @@ class DebianConfigurator(configurator.ApacheConfigurator):
# Generate reversal command.
# Try to be safe here... check that we can probably reverse before
# applying enmod command
if not util.exe_exists(self.conf("dismod")):
if not util.exe_exists(self.option("dismod")):
raise errors.MisconfigurationError(
"Unable to find a2dismod, please make sure a2enmod and "
"a2dismod are configured correctly for certbot.")
self.reverter.register_undo_command(
temp, [self.conf("dismod"), "-f", mod_name])
util.run_script([self.conf("enmod"), mod_name])
temp, [self.option("dismod"), "-f", mod_name])
util.run_script([self.option("enmod"), mod_name])
@@ -18,25 +18,33 @@ class GentooConfigurator(configurator.ApacheConfigurator):
vhost_root="/etc/apache2/vhosts.d",
vhost_files="*.conf",
logs_root="/var/log/apache2",
version_cmd=['/usr/sbin/apache2', '-v'],
apache_cmd="apache2ctl",
ctl="apache2ctl",
version_cmd=['apache2ctl', '-v'],
restart_cmd=['apache2ctl', 'graceful'],
restart_cmd_alt=['apache2ctl', 'restart'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_mods=False,
handle_modules=False,
handle_sites=False,
challenge_location="/etc/apache2/vhosts.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "options-ssl-apache.conf")
)
def _prepare_options(self):
"""
Override the options dictionary initialization in order to support
alternative restart cmd used in Gentoo.
"""
super(GentooConfigurator, self)._prepare_options()
self.options["restart_cmd_alt"][0] = self.option("ctl")
def get_parser(self):
"""Initializes the ApacheParser"""
return GentooParser(
self.aug, self.conf("server-root"), self.conf("vhost-root"),
self.aug, self.option("server_root"), self.option("vhost_root"),
self.version, configurator=self)
@@ -61,7 +69,7 @@ class GentooParser(parser.ApacheParser):
def update_modules(self):
"""Get loaded modules from httpd process, and add them to DOM"""
mod_cmd = [self.configurator.constant("apache_cmd"), "modules"]
mod_cmd = [self.configurator.option("ctl"), "modules"]
matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module")
for mod in matches:
self.add_mod(mod.strip())
@@ -16,14 +16,14 @@ class OpenSUSEConfigurator(configurator.ApacheConfigurator):
vhost_root="/etc/apache2/vhosts.d",
vhost_files="*.conf",
logs_root="/var/log/apache2",
ctl="apache2ctl",
version_cmd=['apache2ctl', '-v'],
apache_cmd="apache2ctl",
restart_cmd=['apache2ctl', 'graceful'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod="a2enmod",
dismod="a2dismod",
le_vhost_ext="-le-ssl.conf",
handle_mods=False,
handle_modules=False,
handle_sites=False,
challenge_location="/etc/apache2/vhosts.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
+4 -4
View File
@@ -69,7 +69,7 @@ class ApacheParser(object):
# Must also attempt to parse additional virtual host root
if vhostroot:
self.parse_file(os.path.abspath(vhostroot) + "/" +
self.configurator.constant("vhost_files"))
self.configurator.option("vhost_files"))
# check to see if there were unparsed define statements
if version < (2, 4):
@@ -152,7 +152,7 @@ class ApacheParser(object):
"""Get Defines from httpd process"""
variables = dict()
define_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D",
define_cmd = [self.configurator.option("ctl"), "-t", "-D",
"DUMP_RUN_CFG"]
matches = self.parse_from_subprocess(define_cmd, r"Define: ([^ \n]*)")
try:
@@ -179,7 +179,7 @@ class ApacheParser(object):
# configuration files
_ = self.find_dir("Include")
inc_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D",
inc_cmd = [self.configurator.option("ctl"), "-t", "-D",
"DUMP_INCLUDES"]
matches = self.parse_from_subprocess(inc_cmd, r"\(.*\) (.*)")
if matches:
@@ -190,7 +190,7 @@ class ApacheParser(object):
def update_modules(self):
"""Get loaded modules from httpd process, and add them to DOM"""
mod_cmd = [self.configurator.constant("apache_cmd"), "-t", "-D",
mod_cmd = [self.configurator.option("ctl"), "-t", "-D",
"DUMP_MODULES"]
matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module")
for mod in matches:
@@ -119,6 +119,9 @@ class AutoHSTSTest(util.ApacheTest):
cur_val = maxage.format(constants.AUTOHSTS_STEPS[i+1])
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
cur_val)
# Ensure that the value is raised to max
self.assertEquals(self.get_autohsts_value(self.vh_truth[7].path),
maxage.format(constants.AUTOHSTS_STEPS[-1]))
# Make permanent
self.config.deploy_autohsts(mock_lineage)
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
@@ -135,5 +135,7 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
errors.SubprocessError,
errors.SubprocessError]
self.assertRaises(errors.MisconfigurationError, self.config.restart)
if __name__ == "__main__":
unittest.main() # pragma: no cover
@@ -115,9 +115,22 @@ class MultipleVhostsTest(util.ApacheTest):
# Weak test..
ApacheConfigurator.add_parser_arguments(mock.MagicMock())
def test_add_parser_arguments_all_configurators(self): # pylint: disable=no-self-use
from certbot_apache.entrypoint import OVERRIDE_CLASSES
for cls in OVERRIDE_CLASSES.values():
cls.add_parser_arguments(mock.MagicMock())
def test_all_configurators_defaults_defined(self):
from certbot_apache.entrypoint import OVERRIDE_CLASSES
from certbot_apache.configurator import ApacheConfigurator
parameters = set(ApacheConfigurator.OS_DEFAULTS.keys())
for cls in OVERRIDE_CLASSES.values():
self.assertTrue(parameters.issubset(set(cls.OS_DEFAULTS.keys())))
def test_constant(self):
self.assertEqual(self.config.constant("server_root"), "/etc/apache2")
self.assertEqual(self.config.constant("nonexistent"), None)
self.assertTrue("debian_apache_2_4/multiple_vhosts/apache" in
self.config.option("server_root"))
self.assertEqual(self.config.option("nonexistent"), None)
@certbot_util.patch_get_utility()
def test_get_all_names(self, mock_getutility):
@@ -651,22 +664,10 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertEqual(ssl_vhost_slink.name, "nonsym.link")
def test_make_vhost_ssl_nonexistent_vhost_path(self):
def conf_side_effect(arg):
""" Mock function for ApacheConfigurator.conf """
confvars = {
"vhost-root": "/tmp/nonexistent",
"le_vhost_ext": "-le-ssl.conf",
"handle-sites": True}
return confvars[arg]
with mock.patch(
"certbot_apache.configurator.ApacheConfigurator.conf"
) as mock_conf:
mock_conf.side_effect = conf_side_effect
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1])
self.assertEqual(os.path.dirname(ssl_vhost.filep),
os.path.dirname(os.path.realpath(
self.vh_truth[1].filep)))
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1])
self.assertEqual(os.path.dirname(ssl_vhost.filep),
os.path.dirname(os.path.realpath(
self.vh_truth[1].filep)))
def test_make_vhost_ssl(self):
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
@@ -1583,7 +1584,7 @@ class AugeasVhostsTest(util.ApacheTest):
broken_vhost)
class MultiVhostsTest(util.ApacheTest):
"""Test vhosts with illegal names dependent on augeas version."""
"""Test configuration with multiple virtualhosts in a single file."""
# pylint: disable=protected-access
def setUp(self): # pylint: disable=arguments-differ
@@ -1703,7 +1704,7 @@ class InstallSslOptionsConfTest(util.ApacheTest):
self.config.updated_mod_ssl_conf_digest)
def _current_ssl_options_hash(self):
return crypto_util.sha256sum(self.config.constant("MOD_SSL_CONF_SRC"))
return crypto_util.sha256sum(self.config.option("MOD_SSL_CONF_SRC"))
def _assert_current_file(self):
self.assertTrue(os.path.isfile(self.config.mod_ssl_conf))
@@ -1739,7 +1740,7 @@ class InstallSslOptionsConfTest(util.ApacheTest):
self.assertFalse(mock_logger.warning.called)
self.assertTrue(os.path.isfile(self.config.mod_ssl_conf))
self.assertEqual(crypto_util.sha256sum(
self.config.constant("MOD_SSL_CONF_SRC")),
self.config.option("MOD_SSL_CONF_SRC")),
self._current_ssl_options_hash())
self.assertNotEqual(crypto_util.sha256sum(self.config.mod_ssl_conf),
self._current_ssl_options_hash())
@@ -1755,7 +1756,7 @@ class InstallSslOptionsConfTest(util.ApacheTest):
"%s has been manually modified; updated file "
"saved to %s. We recommend updating %s for security purposes.")
self.assertEqual(crypto_util.sha256sum(
self.config.constant("MOD_SSL_CONF_SRC")),
self.config.option("MOD_SSL_CONF_SRC")),
self._current_ssl_options_hash())
# only print warning once
with mock.patch("certbot.plugins.common.logger") as mock_logger:
@@ -20,7 +20,7 @@ class MultipleVhostsTestDebian(util.ApacheTest):
def setUp(self): # pylint: disable=arguments-differ
super(MultipleVhostsTestDebian, self).setUp()
self.config = util.get_apache_configurator(
self.config_path, None, self.config_dir, self.work_dir,
self.config_path, self.vhost_path, self.config_dir, self.work_dir,
os_info="debian")
self.config = self.mock_deploy_cert(self.config)
self.vh_truth = util.get_vh_truth(self.temp_dir,
@@ -117,7 +117,7 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
self.config.parser.modules = set()
with mock.patch("certbot.util.get_os_info") as mock_osi:
# Make sure we have the have the CentOS httpd constants
# Make sure we have the have the Gentoo httpd constants
mock_osi.return_value = ("gentoo", "123")
self.config.parser.update_runtime_variables()
@@ -10,6 +10,7 @@ from certbot import achallenges
from certbot import errors
from certbot.tests import acme_util
from certbot_apache.parser import get_aug_path
from certbot_apache.tests import util
@@ -134,6 +135,21 @@ class ApacheHttp01Test(util.ApacheTest):
def test_perform_3_achall_apache_2_4(self):
self.combinations_perform_test(num_achalls=3, minor_version=4)
def test_activate_disabled_vhost(self):
vhosts = [v for v in self.config.vhosts if v.name == "certbot.demo"]
achalls = [
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.HTTP01(token=((b'a' * 16))),
"pending"),
domain="certbot.demo", account_key=self.account_key)]
vhosts[0].enabled = False
self.common_perform_test(achalls, vhosts)
matches = self.config.parser.find_dir(
"Include", vhosts[0].filep,
get_aug_path(self.config.parser.loc["default"]))
self.assertEqual(len(matches), 1)
def combinations_perform_test(self, num_achalls, minor_version):
"""Test perform with the given achall count and Apache version."""
achalls = self.achalls[:num_achalls]
@@ -282,11 +282,11 @@ class BasicParserTest(util.ParserTest):
self.assertRaises(
errors.PluginError, self.parser.update_runtime_variables)
@mock.patch("certbot_apache.configurator.ApacheConfigurator.constant")
@mock.patch("certbot_apache.configurator.ApacheConfigurator.option")
@mock.patch("certbot_apache.parser.subprocess.Popen")
def test_update_runtime_vars_bad_ctl(self, mock_popen, mock_const):
def test_update_runtime_vars_bad_ctl(self, mock_popen, mock_opt):
mock_popen.side_effect = OSError
mock_const.return_value = "nonexistent"
mock_opt.return_value = "nonexistent"
self.assertRaises(
errors.MisconfigurationError,
self.parser.update_runtime_variables)
+21 -28
View File
@@ -97,9 +97,10 @@ def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-loc
backups = os.path.join(work_dir, "backups")
mock_le_config = mock.MagicMock(
apache_server_root=config_path,
apache_vhost_root=conf_vhost_path,
apache_vhost_root=None,
apache_le_vhost_ext="-le-ssl.conf",
apache_challenge_location=config_path,
apache_enmod=None,
backup_dir=backups,
config_dir=config_dir,
http01_port=80,
@@ -107,33 +108,25 @@ def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-loc
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
work_dir=work_dir)
orig_os_constant = configurator.ApacheConfigurator(mock_le_config,
name="apache",
version=version).constant
def mock_os_constant(key, vhost_path=vhost_path):
"""Mock default vhost path"""
if key == "vhost_root":
return vhost_path
else:
return orig_os_constant(key)
with mock.patch("certbot_apache.configurator.ApacheConfigurator.constant") as mock_cons:
mock_cons.side_effect = mock_os_constant
with mock.patch("certbot_apache.configurator.util.run_script"):
with mock.patch("certbot_apache.configurator.util."
"exe_exists") as mock_exe_exists:
mock_exe_exists.return_value = True
with mock.patch("certbot_apache.parser.ApacheParser."
"update_runtime_variables"):
try:
config_class = entrypoint.OVERRIDE_CLASSES[os_info]
except KeyError:
config_class = configurator.ApacheConfigurator
config = config_class(config=mock_le_config, name="apache",
version=version)
config.prepare()
with mock.patch("certbot_apache.configurator.util.run_script"):
with mock.patch("certbot_apache.configurator.util."
"exe_exists") as mock_exe_exists:
mock_exe_exists.return_value = True
with mock.patch("certbot_apache.parser.ApacheParser."
"update_runtime_variables"):
try:
config_class = entrypoint.OVERRIDE_CLASSES[os_info]
except KeyError:
config_class = configurator.ApacheConfigurator
config = config_class(config=mock_le_config, name="apache",
version=version)
if not conf_vhost_path:
config_class.OS_DEFAULTS["vhost_root"] = vhost_path
else:
# Custom virtualhost path was requested
config.config.apache_vhost_root = conf_vhost_path
config.config.apache_ctl = config_class.OS_DEFAULTS["ctl"]
config.prepare()
return config
+2 -2
View File
@@ -2,13 +2,13 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme>=0.25.0',
'certbot>=0.26.0.dev0',
'certbot>=0.26.0',
'mock',
'python-augeas',
'setuptools',
+36 -47
View File
@@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
fi
VENV_BIN="$VENV_PATH/bin"
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
LE_AUTO_VERSION="0.25.1"
LE_AUTO_VERSION="0.27.1"
BASENAME=$(basename $0)
USAGE="Usage: $BASENAME [OPTIONS]
A self-updating wrapper script for the Certbot ACME client. When run, updates
@@ -1060,37 +1060,26 @@ ConfigArgParse==0.12.0 \
configobj==5.0.6 \
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 \
--no-binary configobj
cryptography==2.0.2 \
--hash=sha256:187ae17358436d2c760f28c2aeb02fefa3f37647a9c5b6f7f7c3e83cd1c5a972 \
--hash=sha256:19e43a13bbf52028dd1e810c803f2ad8880d0692d772f98d42e1eaf34bdee3d6 \
--hash=sha256:da9291502cbc87dc0284a20c56876e4d2e68deac61cc43df4aec934e44ca97b1 \
--hash=sha256:0954f8813095f581669330e0a2d5e726c33ac7f450c1458fac58bab54595e516 \
--hash=sha256:d68b0cc40a8432ed3fc84876c519de704d6001800ec22b136e75ae841910c45b \
--hash=sha256:2f8ad9580ab4da645cfea52a91d2da99a49a1e76616d8be68441a986fad652b0 \
--hash=sha256:cc00b4511294f5f6b65c4e77a1a9c62f52490a63d2c120f3872176b40a82351e \
--hash=sha256:cf896020f6a9f095a547b3d672c8db1ef2ed71fca11250731fa1d4a4cb8b1590 \
--hash=sha256:e0fdb8322206fa02aa38f71519ff75dce2eb481b7e1110e2936795cb376bb6ee \
--hash=sha256:277538466657ca5d6637f80be100242f9831d75138b788d718edd3aab34621f8 \
--hash=sha256:2c77eb0560f54ce654ab82d6b2a64327a71ee969b29022bf9746ca311c9f5069 \
--hash=sha256:755a7853b679e79d0a799351c092a9b0271f95ff54c8dd8823d8b527a2926a86 \
--hash=sha256:77197a2d525e761cdd4c771180b4bd0d80703654c6385e4311cbbbe2beb56fa1 \
--hash=sha256:eb8bb79d0ab00c931c8333b745f06fec481a51c52d70acd4ee95d6093ba5c386 \
--hash=sha256:131f61de82ef28f3e20beb4bfc24f9692d28cecfd704e20e6c7f070f7793013a \
--hash=sha256:ac35435974b2e27cd4520f29c191d7da36f4189aa3264e52c4c6c6d089ab6142 \
--hash=sha256:04b6ea99daa2a8460728794213d76d45ad58ea247dc7e7ff148d7dd726e87863 \
--hash=sha256:2b9442f8b4c3d575f6cc3db0e856034e0f5a9d55ecd636f52d8c496795b26952 \
--hash=sha256:b3d3b3ecba1fe1bdb6f180770a137f877c8f07571f7b2934bb269475bcf0e5e8 \
--hash=sha256:670a58c0d75cb0e78e73dd003bd96d4440bbb1f2bc041dcf7b81767ca4fb0ce9 \
--hash=sha256:5af84d23bdb86b5e90aca263df1424b43f1748480bfcde3ac2a3cbe622612468 \
--hash=sha256:ba22e8eefabdd7aca37d0c0c00d2274000d2cebb5cce9e5a710cb55bf8797b31 \
--hash=sha256:b798b22fa7e92b439547323b8b719d217f1e1b7677585cfeeedf3b55c70bb7fb \
--hash=sha256:59cff28af8cce96cb7e94a459726e1d88f6f5fa75097f9dcbebd99118d64ea4c \
--hash=sha256:fe859e445abc9ba9e97950ddafb904e23234c4ecb76b0fae6c86e80592ce464a \
--hash=sha256:655f3c474067f1e277430f23cc0549f0b1dc99b82aec6e53f80b9b2db7f76f11 \
--hash=sha256:0ebc2be053c9a03a2f3e20a466e87bf12a51586b3c79bd2a22171b073a805346 \
--hash=sha256:01e6e60654df64cca53733cda39446d67100c819c181d403afb120e0d2a71e1b \
--hash=sha256:d46f4e5d455cb5563685c52ef212696f0a6cc1ea627603218eabbd8a095291d8 \
--hash=sha256:3780b2663ee7ebb37cb83263326e3cd7f8b2ea439c448539d4b87de12c8d06ab
cryptography==2.2.2 \
--hash=sha256:3f3b65d5a16e6b52fba63dc860b62ca9832f51f1a2ae5083c78b6840275f12dd \
--hash=sha256:5251e7de0de66810833606439ca65c9b9e45da62196b0c88bfadf27740aac09f \
--hash=sha256:551a3abfe0c8c6833df4192a63371aa2ff43afd8f570ed345d31f251d78e7e04 \
--hash=sha256:5cb990056b7cadcca26813311187ad751ea644712022a3976443691168781b6f \
--hash=sha256:60bda7f12ecb828358be53095fc9c6edda7de8f1ef571f96c00b2363643fa3cd \
--hash=sha256:64b5c67acc9a7c83fbb4b69166f3105a0ab722d27934fac2cb26456718eec2ba \
--hash=sha256:6fef51ec447fe9f8351894024e94736862900d3a9aa2961528e602eb65c92bdb \
--hash=sha256:77d0ad229d47a6e0272d00f6bf8ac06ce14715a9fd02c9a97f5a2869aab3ccb2 \
--hash=sha256:808fe471b1a6b777f026f7dc7bd9a4959da4bfab64972f2bbe91e22527c1c037 \
--hash=sha256:9b62fb4d18529c84b961efd9187fecbb48e89aa1a0f9f4161c61b7fc42a101bd \
--hash=sha256:9e5bed45ec6b4f828866ac6a6bedf08388ffcfa68abe9e94b34bb40977aba531 \
--hash=sha256:9fc295bf69130a342e7a19a39d7bbeb15c0bcaabc7382ec33ef3b2b7d18d2f63 \
--hash=sha256:abd070b5849ed64e6d349199bef955ee0ad99aefbad792f0c587f8effa681a5e \
--hash=sha256:ba6a774749b6e510cffc2fb98535f717e0e5fd91c7c99a61d223293df79ab351 \
--hash=sha256:c332118647f084c983c6a3e1dba0f3bcb051f69d12baccac68db8d62d177eb8a \
--hash=sha256:d6f46e862ee36df81e6342c2177ba84e70f722d9dc9c6c394f9f1f434c4a5563 \
--hash=sha256:db6013746f73bf8edd9c3d1d3f94db635b9422f503db3fc5ef105233d4c011ab \
--hash=sha256:f57008eaff597c69cf692c3518f6d4800f0309253bb138b526a37fe9ef0c7471 \
--hash=sha256:f6c821ac253c19f2ad4c8691633ae1d1a17f120d5b01ea1d256d7b602bc59887
enum34==1.1.2 ; python_version < '3.4' \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
@@ -1103,9 +1092,9 @@ idna==2.5 \
ipaddress==1.0.16 \
--hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \
--hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0
josepy==1.0.1 \
--hash=sha256:354a3513038a38bbcd27c97b7c68a8f3dfaff0a135b20a92c6db4cc4ea72915e \
--hash=sha256:9f48b88ca37f0244238b1cc77723989f7c54f7b90b2eee6294390bacfe870acc
josepy==1.1.0 \
--hash=sha256:1309a25aac3caeff5239729c58ff9b583f7d022ffdb1553406ddfc8e5b52b76e \
--hash=sha256:fb5c62c77d26e04df29cb5ecd01b9ce69b6fcc9e521eb1ca193b7faa2afa7086
linecache2==1.0.0 \
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
@@ -1208,18 +1197,18 @@ letsencrypt==0.7.0 \
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
certbot==0.25.1 \
--hash=sha256:01689015364685fef3f1e1fb7832ba84eb3b0aa85bc5a71c96661f6d4c59981f \
--hash=sha256:5c23e5186133bb1afd805be5e0cd2fb7b95862a8b0459c9ecad4ae60f933e54e
acme==0.25.1 \
--hash=sha256:26e641a01536705fe5f12d856703b8ef06e5a07981a7b6379d2771dcdb69a742 \
--hash=sha256:47b5f3f73d69b7b1d13f918aa2cd75a8093069a68becf4af38e428e4613b2734
certbot-apache==0.25.1 \
--hash=sha256:a28b7c152cc11474bef5b5e7967aaea42b2c0aaf86fd82ee4082713d33cee5a9 \
--hash=sha256:ed012465617073a0f1057fe854dc8d1eb6d2dd7ede1fb2eee765129fed2a095a
certbot-nginx==0.25.1 \
--hash=sha256:83f82c3ba08c0b1d4bf449ac24018e8e7dd34a6248d35466f2de7da1cd312e15 \
--hash=sha256:68f98b41c54e0bf4218ef293079597176617bee3837ae3aa6528ce2ff0bf4f9c
certbot==0.27.1 \
--hash=sha256:89a8d8e44e272ee970259c93fa2ff2c9f063da8fd88a56d7ca30d7a2218791ea \
--hash=sha256:3570bd14ed223c752f309dbd082044bd9f11a339d21671e70a2eeae4e51ed02a
acme==0.27.1 \
--hash=sha256:0d42cfc9050a2e1d6d4e6b66334df8173778db0b3fe7a2b3bcb58f7034913597 \
--hash=sha256:31a7b9023ce183616e6ebd5d783e842c3d68696ff70db59a06db9feea8f54f90
certbot-apache==0.27.1 \
--hash=sha256:1c73297e6a59cebcf5f5692025d4013ccd02c858bdc946fee3c6613f62bb9414 \
--hash=sha256:61d6d706d49d726b53a831a2ea9099bd6c02657ff537a166dd197cd5f494d854
certbot-nginx==0.27.1 \
--hash=sha256:9772198bcfde9b68e448c15c3801b3cf9d20eb9ea9da1d9f4f9a7692b0fc2314 \
--hash=sha256:ff5b849a9b4e3d1fd50ea351a1393738382fc9bd47bc5ac18c343d11a691349f
UNLIKELY_EOF
# -------------------------------------------------------------------------
+1 -1
View File
@@ -14,7 +14,7 @@ RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only
# the above is not likely to change, so by putting it further up the
# Dockerfile we make sure we cache as much as possible
COPY setup.py README.rst CHANGES.rst MANIFEST.in linter_plugin.py tox.cover.sh tox.ini .pylintrc /opt/certbot/src/
COPY setup.py README.rst CHANGELOG.md MANIFEST.in linter_plugin.py tox.cover.sh tox.ini .pylintrc /opt/certbot/src/
# all above files are necessary for setup.py, however, package source
# code directory has to be copied separately to a subdirectory...
@@ -59,9 +59,6 @@ class Proxy(configurators_common.Proxy):
setattr(self.le_config, "apache_" + k,
entrypoint.ENTRYPOINT.OS_DEFAULTS[k])
# An alias
self.le_config.apache_handle_modules = self.le_config.apache_handle_mods
self._configurator = entrypoint.ENTRYPOINT(
config=configuration.NamespaceConfig(self.le_config),
name="apache")
+1 -1
View File
@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
install_requires = [
'certbot',
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
+1 -1
View File
@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.25.0.dev0'
version = '0.28.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
@@ -14,7 +14,11 @@ Named Arguments
DNS to propagate before asking the
ACME server to verify the DNS
record.
(Default: 960)
(Default: 1200 because Linode
updates its first DNS every 15
minutes and we allow 5 more minutes
for the update to reach the other 5
servers)
========================================== ===================================
@@ -23,7 +27,7 @@ Credentials
Use of this plugin requires a configuration file containing Linode API
credentials, obtained from your Linode account's `Applications & API
Tokens page <https://cloud.linode.com/settings/api/tokens>`_.
Tokens page <https://manager.linode.com/profile/api>`_.
.. code-block:: ini
:name: credentials.ini
@@ -74,13 +78,15 @@ Examples
-d www.example.com
.. code-block:: bash
:caption: To acquire a certificate for ``example.com``, waiting 60 seconds
for DNS propagation
:caption: To acquire a certificate for ``example.com``, waiting 1000 seconds
for DNS propagation (Linode updates its first DNS every 15 minutes
and we allow some extra time for the update to reach the other 5
servers)
certbot certonly \\
--dns-linode \\
--dns-linode-credentials ~/.secrets/certbot/linode.ini \\
--dns-linode-propagation-seconds 60 \\
--dns-linode-propagation-seconds 1000 \\
-d example.com
"""
@@ -29,7 +29,7 @@ class Authenticator(dns_common.DNSAuthenticator):
@classmethod
def add_parser_arguments(cls, add): # pylint: disable=arguments-differ
super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=960)
super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=1200)
add('credentials', help='Linode credentials INI file.')
def more_info(self): # pylint: disable=missing-docstring,no-self-use
+1 -1
View File
@@ -3,7 +3,7 @@ import sys
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
+3 -5
View File
@@ -4,18 +4,16 @@ from setuptools import setup
from setuptools import find_packages
version = '0.25.0.dev0'
version = '0.28.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>=2.2.1', # Support for >1 TXT record per name
'dns-lexicon>=2.7.3', # Correct OVH integration tests
'mock',
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'setuptools',
'zope.interface',
]
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
+1 -1
View File
@@ -1,7 +1,7 @@
from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
@@ -38,7 +38,8 @@ class AuthenticatorTest(test_util.TempDirTestCase,
self.auth._get_sakuracloud_client = mock.MagicMock(return_value=self.mock_client)
class NS1LexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
class SakuraCloudLexiconClientTest(unittest.TestCase,
dns_test_common_lexicon.BaseLexiconClientTest):
DOMAIN_NOT_FOUND = HTTPError('404 Client Error: Not Found for url: {0}.'.format(DOMAIN))
LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized for url: {0}.'.format(DOMAIN))
+1 -1
View File
@@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.25.0.dev0'
version = '0.28.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
+35 -13
View File
@@ -8,7 +8,6 @@ import tempfile
import time
import OpenSSL
import six
import zope.interface
from acme import challenges
@@ -32,6 +31,12 @@ from certbot_nginx import obj # pylint: disable=unused-import
from acme.magic_typing import List, Dict, Set # pylint: disable=unused-import, no-name-in-module
NAME_RANK = 0
START_WILDCARD_RANK = 1
END_WILDCARD_RANK = 2
REGEX_RANK = 3
NO_SSL_MODIFIER = 4
logger = logging.getLogger(__name__)
@@ -136,7 +141,9 @@ class NginxConfigurator(common.Installer):
"""
# Verify Nginx is installed
if not util.exe_exists(self.conf('ctl')):
raise errors.NoInstallationError
raise errors.NoInstallationError(
"Could not find a usable 'nginx' binary. Ensure nginx exists, "
"the binary is executable, and your PATH is set correctly.")
# Make sure configuration is valid
self.config_test()
@@ -403,7 +410,8 @@ class NginxConfigurator(common.Installer):
"""
if not matches:
return None
elif matches[0]['rank'] in six.moves.range(2, 6):
elif matches[0]['rank'] in [START_WILDCARD_RANK, END_WILDCARD_RANK,
START_WILDCARD_RANK + NO_SSL_MODIFIER, END_WILDCARD_RANK + NO_SSL_MODIFIER]:
# Wildcard match - need to find the longest one
rank = matches[0]['rank']
wildcards = [x for x in matches if x['rank'] == rank]
@@ -412,10 +420,9 @@ class NginxConfigurator(common.Installer):
# Exact or regex match
return matches[0]['vhost']
def _rank_matches_by_name_and_ssl(self, vhost_list, target_name):
def _rank_matches_by_name(self, vhost_list, target_name):
"""Returns a ranked list of vhosts from vhost_list that match target_name.
The ranking gives preference to SSL vhosts.
This method should always be followed by a call to _select_best_name_match.
:param list vhost_list: list of vhosts to filter and rank
:param str target_name: The name to match
@@ -435,21 +442,37 @@ class NginxConfigurator(common.Installer):
if name_type == 'exact':
matches.append({'vhost': vhost,
'name': name,
'rank': 0 if vhost.ssl else 1})
'rank': NAME_RANK})
elif name_type == 'wildcard_start':
matches.append({'vhost': vhost,
'name': name,
'rank': 2 if vhost.ssl else 3})
'rank': START_WILDCARD_RANK})
elif name_type == 'wildcard_end':
matches.append({'vhost': vhost,
'name': name,
'rank': 4 if vhost.ssl else 5})
'rank': END_WILDCARD_RANK})
elif name_type == 'regex':
matches.append({'vhost': vhost,
'name': name,
'rank': 6 if vhost.ssl else 7})
'rank': REGEX_RANK})
return sorted(matches, key=lambda x: x['rank'])
def _rank_matches_by_name_and_ssl(self, vhost_list, target_name):
"""Returns a ranked list of vhosts from vhost_list that match target_name.
The ranking gives preference to SSLishness before name match level.
:param list vhost_list: list of vhosts to filter and rank
:param str target_name: The name to match
:returns: list of dicts containing the vhost, the matching name, and
the numerical rank
:rtype: list
"""
matches = self._rank_matches_by_name(vhost_list, target_name)
for match in matches:
if not match['vhost'].ssl:
match['rank'] += NO_SSL_MODIFIER
return sorted(matches, key=lambda x: x['rank'])
def choose_redirect_vhosts(self, target_name, port, create_if_no_match=False):
"""Chooses a single virtual host for redirect enhancement.
@@ -529,9 +552,7 @@ class NginxConfigurator(common.Installer):
matching_vhosts = [vhost for vhost in all_vhosts if _vhost_matches(vhost, port)]
# We can use this ranking function because sslishness doesn't matter to us, and
# there shouldn't be conflicting plaintextish servers listening on 80.
return self._rank_matches_by_name_and_ssl(matching_vhosts, target_name)
return self._rank_matches_by_name(matching_vhosts, target_name)
def get_all_names(self):
"""Returns all names found in the Nginx Configuration.
@@ -566,6 +587,7 @@ class NginxConfigurator(common.Installer):
return util.get_filtered_names(all_names)
def _get_snakeoil_paths(self):
"""Generate invalid certs that let us create ssl directives for Nginx"""
# TODO: generate only once
tmp_dir = os.path.join(self.config.work_dir, "snakeoil")
le_key = crypto_util.init_save_key(
+2 -5
View File
@@ -40,8 +40,6 @@ class NginxHttp01(common.ChallengePerformer):
super(NginxHttp01, self).__init__(configurator)
self.challenge_conf = os.path.join(
configurator.config.config_dir, "le_http_01_cert_challenge.conf")
self._ipv6 = None
self._ipv6only = None
def perform(self):
"""Perform a challenge on Nginx.
@@ -102,6 +100,7 @@ class NginxHttp01(common.ChallengePerformer):
config = [self._make_or_mod_server_block(achall) for achall in self.achalls]
config = [x for x in config if x is not None]
config = nginxparser.UnspacedList(config)
logger.debug("Generated server block:\n%s", str(config))
self.configurator.reverter.register_file_creation(
True, self.challenge_conf)
@@ -120,9 +119,7 @@ class NginxHttp01(common.ChallengePerformer):
self.configurator.config.http01_port)
port = self.configurator.config.http01_port
if self._ipv6 is None or self._ipv6only is None:
self._ipv6, self._ipv6only = self.configurator.ipv6_info(port)
ipv6, ipv6only = self._ipv6, self._ipv6only
ipv6, ipv6only = self.configurator.ipv6_info(port)
if ipv6:
# If IPv6 is active in Nginx configuration
+1 -1
View File
@@ -26,7 +26,7 @@ class RawNginxParser(object):
dquoted = QuotedString('"', multiline=True, unquoteResults=False, escChar='\\')
squoted = QuotedString("'", multiline=True, unquoteResults=False, escChar='\\')
quoted = dquoted | squoted
head_tokenchars = Regex(r"[^{};\s'\"]") # if (last_space)
head_tokenchars = Regex(r"(\$\{)|[^{};\s'\"]") # if (last_space)
tail_tokenchars = Regex(r"(\$\{)|[^{;\s]") # else
tokenchars = Combine(head_tokenchars + ZeroOrMore(tail_tokenchars))
paren_quote_extend = Combine(quoted + Literal(')') + ZeroOrMore(tail_tokenchars))
+12 -7
View File
@@ -222,7 +222,7 @@ class NginxParser(object):
return os.path.join(self.root, name)
raise errors.NoInstallationError(
"Could not find configuration root")
"Could not find Nginx root configuration file (nginx.conf)")
def filedump(self, ext='tmp', lazy=True):
"""Dumps parsed configurations into files.
@@ -395,12 +395,17 @@ class NginxParser(object):
addr.ipv6only = False
for directive in enclosing_block[new_vhost.path[-1]][1]:
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')]
# Exclude one-time use parameters which will cause an error if repeated.
# https://nginx.org/en/docs/http/ngx_http_core_module.html#listen
exclude = set(('default_server', 'default', 'setfib', 'fastopen', 'backlog',
'rcvbuf', 'sndbuf', 'accept_filter', 'deferred', 'bind',
'ipv6only', 'reuseport', 'so_keepalive'))
for param in exclude:
# See: github.com/certbot/certbot/pull/6223#pullrequestreview-143019225
keys = [x.split('=')[0] for x in directive]
if param in keys:
del directive[keys.index(param)]
return new_vhost
+392
View File
@@ -0,0 +1,392 @@
""" This file contains parsing routines and object classes to help derive meaning from
raw lists of tokens from pyparsing. """
import abc
import logging
import six
from certbot import errors
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
logger = logging.getLogger(__name__)
COMMENT = " managed by Certbot"
COMMENT_BLOCK = ["#", COMMENT]
class Parsable(object):
""" Abstract base class for "Parsable" objects whose underlying representation
is a tree of lists.
:param .Parsable parent: This object's parsed parent in the tree
"""
__metaclass__ = abc.ABCMeta
def __init__(self, parent=None):
self._data = [] # type: List[object]
self._tabs = None
self.parent = parent
@classmethod
def parsing_hooks(cls):
"""Returns object types that this class should be able to `parse` recusrively.
The order of the objects indicates the order in which the parser should
try to parse each subitem.
:returns: A list of Parsable classes.
:rtype list:
"""
return (Block, Sentence, Statements)
@staticmethod
@abc.abstractmethod
def should_parse(lists):
""" Returns whether the contents of `lists` can be parsed into this object.
:returns: Whether `lists` can be parsed as this object.
:rtype bool:
"""
raise NotImplementedError()
@abc.abstractmethod
def parse(self, raw_list, add_spaces=False):
""" Loads information into this object from underlying raw_list structure.
Each Parsable object might make different assumptions about the structure of
raw_list.
:param list raw_list: A list or sublist of tokens from pyparsing, containing whitespace
as separate tokens.
:param bool add_spaces: If set, the method can and should manipulate and insert spacing
between non-whitespace tokens and lists to delimit them.
:raises .errors.MisconfigurationError: when the assumptions about the structure of
raw_list are not met.
"""
raise NotImplementedError()
@abc.abstractmethod
def iterate(self, expanded=False, match=None):
""" Iterates across this object. If this object is a leaf object, only yields
itself. If it contains references other parsing objects, and `expanded` is set,
this function should first yield itself, then recursively iterate across all of them.
:param bool expanded: Whether to recursively iterate on possible children.
:param callable match: If provided, an object is only iterated if this callable
returns True when called on that object.
:returns: Iterator over desired objects.
"""
raise NotImplementedError()
@abc.abstractmethod
def get_tabs(self):
""" Guess at the tabbing style of this parsed object, based on whitespace.
If this object is a leaf, it deducts the tabbing based on its own contents.
Other objects may guess by calling `get_tabs` recursively on child objects.
:returns: Guess at tabbing for this object. Should only return whitespace strings
that does not contain newlines.
:rtype str:
"""
raise NotImplementedError()
@abc.abstractmethod
def set_tabs(self, tabs=" "):
"""This tries to set and alter the tabbing of the current object to a desired
whitespace string. Primarily meant for objects that were constructed, so they
can conform to surrounding whitespace.
:param str tabs: A whitespace string (not containing newlines).
"""
raise NotImplementedError()
def dump(self, include_spaces=False):
""" Dumps back to pyparsing-like list tree. The opposite of `parse`.
Note: if this object has not been modified, `dump` with `include_spaces=True`
should always return the original input of `parse`.
:param bool include_spaces: If set to False, magically hides whitespace tokens from
dumped output.
:returns: Pyparsing-like list tree.
:rtype list:
"""
return [elem.dump(include_spaces) for elem in self._data]
class Statements(Parsable):
""" A group or list of "Statements". A Statement is either a Block or a Sentence.
The underlying representation is simply a list of these Statement objects, with
an extra `_trailing_whitespace` string to keep track of the whitespace that does not
precede any more statements.
"""
def __init__(self, parent=None):
super(Statements, self).__init__(parent)
self._trailing_whitespace = None
# ======== Begin overridden functions
@staticmethod
def should_parse(lists):
return isinstance(lists, list)
def set_tabs(self, tabs=" "):
""" Sets the tabbing for this set of statements. Does this by calling `set_tabs`
on each of the child statements.
Then, if a parent is present, sets trailing whitespace to parent tabbing. This
is so that the trailing } of any Block that contains Statements lines up
with parent tabbing.
"""
for statement in self._data:
statement.set_tabs(tabs)
if self.parent is not None:
self._trailing_whitespace = "\n" + self.parent.get_tabs()
def parse(self, parse_this, add_spaces=False):
""" Parses a list of statements.
Expects all elements in `parse_this` to be parseable by `type(self).parsing_hooks`,
with an optional whitespace string at the last index of `parse_this`.
"""
if not isinstance(parse_this, list):
raise errors.MisconfigurationError("Statements parsing expects a list!")
# If there's a trailing whitespace in the list of statements, keep track of it.
if len(parse_this) > 0 and isinstance(parse_this[-1], six.string_types) \
and parse_this[-1].isspace():
self._trailing_whitespace = parse_this[-1]
parse_this = parse_this[:-1]
self._data = [parse_raw(elem, self, add_spaces) for elem in parse_this]
def get_tabs(self):
""" Takes a guess at the tabbing of all contained Statements by retrieving the
tabbing of the first Statement."""
if len(self._data) > 0:
return self._data[0].get_tabs()
return ""
def dump(self, include_spaces=False):
""" Dumps this object by first dumping each statement, then appending its
trailing whitespace (if `include_spaces` is set) """
data = super(Statements, self).dump(include_spaces)
if include_spaces and self._trailing_whitespace is not None:
return data + [self._trailing_whitespace]
return data
def iterate(self, expanded=False, match=None):
""" Combines each statement's iterator. """
for elem in self._data:
for sub_elem in elem.iterate(expanded, match):
yield sub_elem
# ======== End overridden functions
def _space_list(list_):
""" Inserts whitespace between adjacent non-whitespace tokens. """
spaced_statement = [] # type: List[str]
for i in reversed(six.moves.xrange(len(list_))):
spaced_statement.insert(0, list_[i])
if i > 0 and not list_[i].isspace() and not list_[i-1].isspace():
spaced_statement.insert(0, " ")
return spaced_statement
class Sentence(Parsable):
""" A list of words. Non-whitespace words are typically separated with whitespace tokens. """
# ======== Begin overridden functions
@staticmethod
def should_parse(lists):
""" Returns True if `lists` can be parseable as a `Sentence`-- that is,
every element is a string type.
:param list lists: The raw unparsed list to check.
:returns: whether this lists is parseable by `Sentence`.
"""
return isinstance(lists, list) and len(lists) > 0 and \
all([isinstance(elem, six.string_types) for elem in lists])
def parse(self, parse_this, add_spaces=False):
""" Parses a list of string types into this object.
If add_spaces is set, adds whitespace tokens between adjacent non-whitespace tokens."""
if add_spaces:
parse_this = _space_list(parse_this)
if not isinstance(parse_this, list) or \
any([not isinstance(elem, six.string_types) for elem in parse_this]):
raise errors.MisconfigurationError("Sentence parsing expects a list of string types.")
self._data = parse_this
def iterate(self, expanded=False, match=None):
""" Simply yields itself. """
if match is None or match(self):
yield self
def set_tabs(self, tabs=" "):
""" Sets the tabbing on this sentence. Inserts a newline and `tabs` at the
beginning of `self._data`. """
if self._data[0].isspace():
return
self._data.insert(0, "\n" + tabs)
def dump(self, include_spaces=False):
""" Dumps this sentence. If include_spaces is set, includes whitespace tokens."""
if not include_spaces:
return self.words
return self._data
def get_tabs(self):
""" Guesses at the tabbing of this sentence. If the first element is whitespace,
returns the whitespace after the rightmost newline in the string. """
first = self._data[0]
if not first.isspace():
return ""
rindex = first.rfind("\n")
return first[rindex+1:]
# ======== End overridden functions
@property
def words(self):
""" Iterates over words, but without spaces. Like Unspaced List. """
return [word.strip("\"\'") for word in self._data if not word.isspace()]
def __getitem__(self, index):
return self.words[index]
def __contains__(self, word):
return word in self.words
class Block(Parsable):
""" Any sort of bloc, denoted by a block name and curly braces, like so:
The parsed block:
block name {
content 1;
content 2;
}
might be represented with the list [names, contents], where
names = ["block", " ", "name", " "]
contents = [["\n ", "content", " ", "1"], ["\n ", "content", " ", "2"], "\n"]
"""
def __init__(self, parent=None):
super(Block, self).__init__(parent)
self.names = None # type: Sentence
self.contents = None # type: Block
@staticmethod
def should_parse(lists):
""" Returns True if `lists` can be parseable as a `Block`-- that is,
it's got a length of 2, the first element is a `Sentence` and the second can be
a `Statements`.
:param list lists: The raw unparsed list to check.
:returns: whether this lists is parseable by `Block`. """
return isinstance(lists, list) and len(lists) == 2 and \
Sentence.should_parse(lists[0]) and isinstance(lists[1], list)
def set_tabs(self, tabs=" "):
""" Sets tabs by setting equivalent tabbing on names, then adding tabbing
to contents."""
self.names.set_tabs(tabs)
self.contents.set_tabs(tabs + " ")
def iterate(self, expanded=False, match=None):
""" Iterator over self, and if expanded is set, over its contents. """
if match is None or match(self):
yield self
if expanded:
for elem in self.contents.iterate(expanded, match):
yield elem
def parse(self, parse_this, add_spaces=False):
""" Parses a list that resembles a block.
The assumptions that this routine makes are:
1. the first element of `parse_this` is a valid Sentence.
2. the second element of `parse_this` is a valid Statement.
If add_spaces is set, we call it recursively on `names` and `contents`, and
add an extra trailing space to `names` (to separate the block's opening bracket
and the block name).
"""
if not Block.should_parse(parse_this):
raise errors.MisconfigurationError("Block parsing expects a list of length 2. "
"First element should be a list of string types (the bloc names), "
"and second should be another list of statements (the bloc content).")
self.names = Sentence(self)
if add_spaces:
parse_this[0].append(" ")
self.names.parse(parse_this[0], add_spaces)
self.contents = Statements(self)
self.contents.parse(parse_this[1], add_spaces)
self._data = [self.names, self.contents]
def get_tabs(self):
""" Guesses tabbing by retrieving tabbing guess of self.names. """
return self.names.get_tabs()
def _is_comment(parsed_obj):
""" Checks whether parsed_obj is a comment.
:param .Parsable parsed_obj:
:returns: whether parsed_obj represents a comment sentence.
:rtype bool:
"""
if not isinstance(parsed_obj, Sentence):
return False
return parsed_obj.words[0] == "#"
def _is_certbot_comment(parsed_obj):
""" Checks whether parsed_obj is a "managed by Certbot" comment.
:param .Parsable parsed_obj:
:returns: whether parsed_obj is a "managed by Certbot" comment.
:rtype bool:
"""
if not _is_comment(parsed_obj):
return False
if len(parsed_obj.words) != len(COMMENT_BLOCK):
return False
for i, word in enumerate(parsed_obj.words):
if word != COMMENT_BLOCK[i]:
return False
return True
def _certbot_comment(parent, preceding_spaces=4):
""" A "Managed by Certbot" comment.
:param int preceding_spaces: Number of spaces between the end of the previous
statement and the comment.
:returns: Sentence containing the comment.
:rtype: .Sentence
"""
result = Sentence(parent)
result.parse([" " * preceding_spaces] + COMMENT_BLOCK)
return result
def _choose_parser(parent, list_):
""" Choose a parser from type(parent).parsing_hooks, depending on whichever hook
returns True first. """
hooks = Parsable.parsing_hooks()
if parent:
hooks = type(parent).parsing_hooks()
for type_ in hooks:
if type_.should_parse(list_):
return type_(parent)
raise errors.MisconfigurationError(
"None of the parsing hooks succeeded, so we don't know how to parse this set of lists.")
def parse_raw(lists_, parent=None, add_spaces=False):
""" Primary parsing factory function.
:param list lists_: raw lists from pyparsing to parse.
:param .Parent parent: The parent containing this object.
:param bool add_spaces: Whether to pass add_spaces to the parser.
:returns .Parsable: The parsed object.
:raises errors.MisconfigurationError: If no parsing hook passes, and we can't
determine which type to parse the raw lists into.
"""
parser = _choose_parser(parent, lists_)
parser.parse(lists_, add_spaces)
return parser
@@ -128,22 +128,39 @@ class NginxConfiguratorTest(util.NginxTest):
['#', parser.COMMENT]]]],
parsed[0])
def test_choose_vhosts(self):
localhost_conf = set(['localhost', r'~^(www\.)?(example|bar)\.'])
server_conf = set(['somename', 'another.alias', 'alias'])
example_conf = set(['.example.com', 'example.*'])
foo_conf = set(['*.www.foo.com', '*.www.example.com'])
ipv6_conf = set(['ipv6.com'])
def test_choose_vhosts_alias(self):
self._test_choose_vhosts_common('alias', 'server_conf')
results = {'localhost': localhost_conf,
'alias': server_conf,
'example.com': example_conf,
'example.com.uk.test': example_conf,
'www.example.com': example_conf,
'test.www.example.com': foo_conf,
'abc.www.foo.com': foo_conf,
'www.bar.co.uk': localhost_conf,
'ipv6.com': ipv6_conf}
def test_choose_vhosts_example_com(self):
self._test_choose_vhosts_common('example.com', 'example_conf')
def test_choose_vhosts_localhost(self):
self._test_choose_vhosts_common('localhost', 'localhost_conf')
def test_choose_vhosts_example_com_uk_test(self):
self._test_choose_vhosts_common('example.com.uk.test', 'example_conf')
def test_choose_vhosts_www_example_com(self):
self._test_choose_vhosts_common('www.example.com', 'example_conf')
def test_choose_vhosts_test_www_example_com(self):
self._test_choose_vhosts_common('test.www.example.com', 'foo_conf')
def test_choose_vhosts_abc_www_foo_com(self):
self._test_choose_vhosts_common('abc.www.foo.com', 'foo_conf')
def test_choose_vhosts_www_bar_co_uk(self):
self._test_choose_vhosts_common('www.bar.co.uk', 'localhost_conf')
def test_choose_vhosts_ipv6_com(self):
self._test_choose_vhosts_common('ipv6.com', 'ipv6_conf')
def _test_choose_vhosts_common(self, name, conf):
conf_names = {'localhost_conf': set(['localhost', r'~^(www\.)?(example|bar)\.']),
'server_conf': set(['somename', 'another.alias', 'alias']),
'example_conf': set(['.example.com', 'example.*']),
'foo_conf': set(['*.www.foo.com', '*.www.example.com']),
'ipv6_conf': set(['ipv6.com'])}
conf_path = {'localhost': "etc_nginx/nginx.conf",
'alias': "etc_nginx/nginx.conf",
@@ -155,22 +172,22 @@ class NginxConfiguratorTest(util.NginxTest):
'www.bar.co.uk': "etc_nginx/nginx.conf",
'ipv6.com': "etc_nginx/sites-enabled/ipv6.com"}
vhost = self.config.choose_vhosts(name)[0]
path = os.path.relpath(vhost.filep, self.temp_dir)
self.assertEqual(conf_names[conf], vhost.names)
self.assertEqual(conf_path[name], path)
# IPv6 specific checks
if name == "ipv6.com":
self.assertTrue(vhost.ipv6_enabled())
# Make sure that we have SSL enabled also for IPv6 addr
self.assertTrue(
any([True for x in vhost.addrs if x.ssl and x.ipv6]))
def test_choose_vhosts_bad(self):
bad_results = ['www.foo.com', 'example', 't.www.bar.co',
'69.255.225.155']
for name in results:
vhost = self.config.choose_vhosts(name)[0]
path = os.path.relpath(vhost.filep, self.temp_dir)
self.assertEqual(results[name], vhost.names)
self.assertEqual(conf_path[name], path)
# IPv6 specific checks
if name == "ipv6.com":
self.assertTrue(vhost.ipv6_enabled())
# Make sure that we have SSL enabled also for IPv6 addr
self.assertTrue(
any([True for x in vhost.addrs if x.ssl and x.ipv6]))
for name in bad_results:
self.assertRaises(errors.MisconfigurationError,
self.config.choose_vhosts, name)
@@ -12,6 +12,7 @@ from certbot import achallenges
from certbot.plugins import common_test
from certbot.tests import acme_util
from certbot_nginx.obj import Addr
from certbot_nginx.tests import util
@@ -108,6 +109,41 @@ class HttpPerformTest(util.NginxTest):
# self.assertEqual(vhost.addrs, set(v_addr2_print))
# self.assertEqual(vhost.names, set([response.z_domain.decode('ascii')]))
@mock.patch("certbot_nginx.configurator.NginxConfigurator.ipv6_info")
def test_default_listen_addresses_no_memoization(self, ipv6_info):
# pylint: disable=protected-access
ipv6_info.return_value = (True, True)
self.http01._default_listen_addresses()
self.assertEqual(ipv6_info.call_count, 1)
ipv6_info.return_value = (False, False)
self.http01._default_listen_addresses()
self.assertEqual(ipv6_info.call_count, 2)
@mock.patch("certbot_nginx.configurator.NginxConfigurator.ipv6_info")
def test_default_listen_addresses_t_t(self, ipv6_info):
# pylint: disable=protected-access
ipv6_info.return_value = (True, True)
addrs = self.http01._default_listen_addresses()
http_addr = Addr.fromstring("80")
http_ipv6_addr = Addr.fromstring("[::]:80")
self.assertEqual(addrs, [http_addr, http_ipv6_addr])
@mock.patch("certbot_nginx.configurator.NginxConfigurator.ipv6_info")
def test_default_listen_addresses_t_f(self, ipv6_info):
# pylint: disable=protected-access
ipv6_info.return_value = (True, False)
addrs = self.http01._default_listen_addresses()
http_addr = Addr.fromstring("80")
http_ipv6_addr = Addr.fromstring("[::]:80 ipv6only=on")
self.assertEqual(addrs, [http_addr, http_ipv6_addr])
@mock.patch("certbot_nginx.configurator.NginxConfigurator.ipv6_info")
def test_default_listen_addresses_f_f(self, ipv6_info):
# pylint: disable=protected-access
ipv6_info.return_value = (False, False)
addrs = self.http01._default_listen_addresses()
http_addr = Addr.fromstring("80")
self.assertEqual(addrs, [http_addr])
if __name__ == "__main__":
unittest.main() # pragma: no cover
@@ -271,6 +271,8 @@ class TestRawNginxParser(unittest.TestCase):
location ~ ^/users/(.+\.(?:gif|jpe?g|png))$ {
alias /data/w3/images/$1;
}
proxy_set_header X-Origin-URI ${scheme}://${http_host}/$request_uri;
"""
parsed = loads(test)
self.assertEqual(parsed, [[['if', '($http_user_agent', '~', 'MSIE)'],
@@ -281,7 +283,8 @@ class TestRawNginxParser(unittest.TestCase):
[['return', '403']]], [['if', '($args', '~', 'post=140)'],
[['rewrite', '^', 'http://example.com/']]],
[['location', '~', '^/users/(.+\\.(?:gif|jpe?g|png))$'],
[['alias', '/data/w3/images/$1']]]]
[['alias', '/data/w3/images/$1']]],
['proxy_set_header', 'X-Origin-URI', '${scheme}://${http_host}/$request_uri']]
)
def test_edge_cases(self):
@@ -289,10 +292,6 @@ class TestRawNginxParser(unittest.TestCase):
parsed = loads(r'"hello\""; # blah "heh heh"')
self.assertEqual(parsed, [['"hello\\""'], ['#', ' blah "heh heh"']])
# empty var as block
parsed = loads(r"${}")
self.assertEqual(parsed, [[['$'], []]])
# if with comment
parsed = loads("""if ($http_cookie ~* "id=([^;]+)(?:;|$)") { # blah )
}""")
@@ -342,10 +341,9 @@ class TestRawNginxParser(unittest.TestCase):
])
# variable weirdness
parsed = loads("directive $var;")
self.assertEqual(parsed, [['directive', '$var']])
parsed = loads("directive $var ${var} $ ${};")
self.assertEqual(parsed, [['directive', '$var', '${var}', '$', '${}']])
self.assertRaises(ParseException, loads, "server {server_name test.com};")
self.assertRaises(ParseException, loads, "directive ${var};")
self.assertEqual(loads("blag${dfgdfg};"), [['blag${dfgdfg}']])
self.assertRaises(ParseException, loads, "blag${dfgdf{g};")
@@ -0,0 +1,253 @@
""" Tests for functions and classes in parser_obj.py """
import unittest
import mock
from certbot_nginx.parser_obj import parse_raw
from certbot_nginx.parser_obj import COMMENT_BLOCK
class CommentHelpersTest(unittest.TestCase):
def test_is_comment(self):
from certbot_nginx.parser_obj import _is_comment
self.assertTrue(_is_comment(parse_raw(['#'])))
self.assertTrue(_is_comment(parse_raw(['#', ' literally anything else'])))
self.assertFalse(_is_comment(parse_raw(['not', 'even', 'a', 'comment'])))
def test_is_certbot_comment(self):
from certbot_nginx.parser_obj import _is_certbot_comment
self.assertTrue(_is_certbot_comment(
parse_raw(COMMENT_BLOCK)))
self.assertFalse(_is_certbot_comment(
parse_raw(['#', ' not a certbot comment'])))
self.assertFalse(_is_certbot_comment(
parse_raw(['#', ' managed by Certbot', ' also not a certbot comment'])))
self.assertFalse(_is_certbot_comment(
parse_raw(['not', 'even', 'a', 'comment'])))
def test_certbot_comment(self):
from certbot_nginx.parser_obj import _certbot_comment, _is_certbot_comment
comment = _certbot_comment(None)
self.assertTrue(_is_certbot_comment(comment))
self.assertEqual(comment.dump(), COMMENT_BLOCK)
self.assertEqual(comment.dump(True), [' '] + COMMENT_BLOCK)
self.assertEqual(_certbot_comment(None, 2).dump(True),
[' '] + COMMENT_BLOCK)
class ParsingHooksTest(unittest.TestCase):
def test_is_sentence(self):
from certbot_nginx.parser_obj import Sentence
self.assertFalse(Sentence.should_parse([]))
self.assertTrue(Sentence.should_parse(['']))
self.assertTrue(Sentence.should_parse(['word']))
self.assertTrue(Sentence.should_parse(['two', 'words']))
self.assertFalse(Sentence.should_parse([[]]))
self.assertFalse(Sentence.should_parse(['word', []]))
def test_is_block(self):
from certbot_nginx.parser_obj import Block
self.assertFalse(Block.should_parse([]))
self.assertFalse(Block.should_parse(['']))
self.assertFalse(Block.should_parse(['two', 'words']))
self.assertFalse(Block.should_parse([[[]], []]))
self.assertFalse(Block.should_parse([['block_name'], ['hi', []], []]))
self.assertFalse(Block.should_parse([['block_name'], 'lol']))
self.assertTrue(Block.should_parse([['block_name'], ['hi', []]]))
self.assertTrue(Block.should_parse([['hello'], []]))
self.assertTrue(Block.should_parse([['block_name'], [['many'], ['statements'], 'here']]))
self.assertTrue(Block.should_parse([['if', ' ', '(whatever)'], ['hi']]))
def test_parse_raw(self):
fake_parser1 = mock.Mock()
fake_parser1.should_parse = lambda x: True
fake_parser2 = mock.Mock()
fake_parser2.should_parse = lambda x: False
# First encountered "match" should parse.
parse_raw([])
fake_parser1.called_once()
fake_parser2.not_called()
fake_parser1.reset_mock()
# "match" that returns False shouldn't parse.
parse_raw([])
fake_parser1.not_called()
fake_parser2.called_once()
@mock.patch("certbot_nginx.parser_obj.Parsable.parsing_hooks")
def test_parse_raw_no_match(self, parsing_hooks):
from certbot import errors
fake_parser1 = mock.Mock()
fake_parser1.should_parse = lambda x: False
parsing_hooks.return_value = (fake_parser1,)
self.assertRaises(errors.MisconfigurationError, parse_raw, [])
parsing_hooks.return_value = tuple()
self.assertRaises(errors.MisconfigurationError, parse_raw, [])
def test_parse_raw_passes_add_spaces(self):
fake_parser1 = mock.Mock()
fake_parser1.should_parse = lambda x: True
parse_raw([])
fake_parser1.parse.called_with([None])
parse_raw([], add_spaces=True)
fake_parser1.parse.called_with([None, True])
class SentenceTest(unittest.TestCase):
def setUp(self):
from certbot_nginx.parser_obj import Sentence
self.sentence = Sentence(None)
def test_parse_bad_sentence_raises_error(self):
from certbot import errors
self.assertRaises(errors.MisconfigurationError, self.sentence.parse, 'lol')
self.assertRaises(errors.MisconfigurationError, self.sentence.parse, [[]])
self.assertRaises(errors.MisconfigurationError, self.sentence.parse, [5])
def test_parse_sentence_words_hides_spaces(self):
og_sentence = ['\r\n', 'hello', ' ', ' ', '\t\n ', 'lol', ' ', 'spaces']
self.sentence.parse(og_sentence)
self.assertEquals(self.sentence.words, ['hello', 'lol', 'spaces'])
self.assertEquals(self.sentence.dump(), ['hello', 'lol', 'spaces'])
self.assertEquals(self.sentence.dump(True), og_sentence)
def test_parse_sentence_with_add_spaces(self):
self.sentence.parse(['hi', 'there'], add_spaces=True)
self.assertEquals(self.sentence.dump(True), ['hi', ' ', 'there'])
self.sentence.parse(['one', ' ', 'space', 'none'], add_spaces=True)
self.assertEquals(self.sentence.dump(True), ['one', ' ', 'space', ' ', 'none'])
def test_iterate(self):
expected = [['1', '2', '3']]
self.sentence.parse(['1', ' ', '2', ' ', '3'])
for i, sentence in enumerate(self.sentence.iterate()):
self.assertEquals(sentence.dump(), expected[i])
def test_set_tabs(self):
self.sentence.parse(['tabs', 'pls'], add_spaces=True)
self.sentence.set_tabs()
self.assertEquals(self.sentence.dump(True)[0], '\n ')
self.sentence.parse(['tabs', 'pls'], add_spaces=True)
def test_get_tabs(self):
self.sentence.parse(['no', 'tabs'])
self.assertEquals(self.sentence.get_tabs(), '')
self.sentence.parse(['\n \n ', 'tabs'])
self.assertEquals(self.sentence.get_tabs(), ' ')
self.sentence.parse(['\n\t ', 'tabs'])
self.assertEquals(self.sentence.get_tabs(), '\t ')
self.sentence.parse(['\n\t \n', 'tabs'])
self.assertEquals(self.sentence.get_tabs(), '')
class BlockTest(unittest.TestCase):
def setUp(self):
from certbot_nginx.parser_obj import Block
self.bloc = Block(None)
self.name = ['server', 'name']
self.contents = [['thing', '1'], ['thing', '2'], ['another', 'one']]
self.bloc.parse([self.name, self.contents])
def test_iterate(self):
# Iterates itself normally
self.assertEquals(self.bloc, next(self.bloc.iterate()))
# Iterates contents while expanded
expected = [self.bloc.dump()] + self.contents
for i, elem in enumerate(self.bloc.iterate(expanded=True)):
self.assertEquals(expected[i], elem.dump())
def test_iterate_match(self):
# can match on contents while expanded
from certbot_nginx.parser_obj import Block, Sentence
expected = [['thing', '1'], ['thing', '2']]
for i, elem in enumerate(self.bloc.iterate(expanded=True,
match=lambda x: isinstance(x, Sentence) and 'thing' in x.words)):
self.assertEquals(expected[i], elem.dump())
# can match on self
self.assertEquals(self.bloc, next(self.bloc.iterate(
expanded=True,
match=lambda x: isinstance(x, Block) and 'server' in x.names)))
def test_parse_with_added_spaces(self):
import copy
self.bloc.parse([copy.copy(self.name), self.contents], add_spaces=True)
self.assertEquals(self.bloc.dump(), [self.name, self.contents])
self.assertEquals(self.bloc.dump(True), [
['server', ' ', 'name', ' '],
[['thing', ' ', '1'],
['thing', ' ', '2'],
['another', ' ', 'one']]])
def test_bad_parse_raises_error(self):
from certbot import errors
self.assertRaises(errors.MisconfigurationError, self.bloc.parse, [[[]], [[]]])
self.assertRaises(errors.MisconfigurationError, self.bloc.parse, ['lol'])
self.assertRaises(errors.MisconfigurationError, self.bloc.parse, ['fake', 'news'])
def test_set_tabs(self):
self.bloc.set_tabs()
self.assertEquals(self.bloc.names.dump(True)[0], '\n ')
for elem in self.bloc.contents.dump(True)[:-1]:
self.assertEquals(elem[0], '\n ')
self.assertEquals(self.bloc.contents.dump(True)[-1][0], '\n')
def test_get_tabs(self):
self.bloc.parse([[' \n \t', 'lol'], []])
self.assertEquals(self.bloc.get_tabs(), ' \t')
class StatementsTest(unittest.TestCase):
def setUp(self):
from certbot_nginx.parser_obj import Statements
self.statements = Statements(None)
self.raw = [
['sentence', 'one'],
['sentence', 'two'],
['and', 'another']
]
self.raw_spaced = [
['\n ', 'sentence', ' ', 'one'],
['\n ', 'sentence', ' ', 'two'],
['\n ', 'and', ' ', 'another'],
'\n\n'
]
def test_set_tabs(self):
self.statements.parse(self.raw)
self.statements.set_tabs()
for statement in self.statements.iterate():
self.assertEquals(statement.dump(True)[0], '\n ')
def test_set_tabs_with_parent(self):
# Trailing whitespace should inherit from parent tabbing.
self.statements.parse(self.raw)
self.statements.parent = mock.Mock()
self.statements.parent.get_tabs.return_value = '\t\t'
self.statements.set_tabs()
for statement in self.statements.iterate():
self.assertEquals(statement.dump(True)[0], '\n ')
self.assertEquals(self.statements.dump(True)[-1], '\n\t\t')
def test_get_tabs(self):
self.raw[0].insert(0, '\n \n \t')
self.statements.parse(self.raw)
self.assertEquals(self.statements.get_tabs(), ' \t')
self.statements.parse([])
self.assertEquals(self.statements.get_tabs(), '')
def test_parse_with_added_spaces(self):
self.statements.parse(self.raw, add_spaces=True)
self.assertEquals(self.statements.dump(True)[0], ['sentence', ' ', 'one'])
def test_parse_bad_list_raises_error(self):
from certbot import errors
self.assertRaises(errors.MisconfigurationError, self.statements.parse, 'lol not a list')
def test_parse_hides_trailing_whitespace(self):
self.statements.parse(self.raw + ['\n\n '])
self.assertTrue(isinstance(self.statements.dump()[-1], list))
self.assertTrue(self.statements.dump(True)[-1].isspace())
self.assertEquals(self.statements.dump(True)[-1], '\n\n ')
def test_iterate(self):
self.statements.parse(self.raw)
expected = [['sentence', 'one'], ['sentence', 'two']]
for i, elem in enumerate(self.statements.iterate(match=lambda x: 'sentence' in x)):
self.assertEquals(expected[i], elem.dump())
if __name__ == "__main__":
unittest.main() # pragma: no cover
+5 -3
View File
@@ -51,9 +51,6 @@ class NginxTlsSni01(common.TLSSNI01):
default_addr = "{0} ssl".format(
self.configurator.config.tls_sni_01_port)
ipv6, ipv6only = self.configurator.ipv6_info(
self.configurator.config.tls_sni_01_port)
for achall in self.achalls:
vhosts = self.configurator.choose_vhosts(achall.domain, create_if_no_match=True)
@@ -61,6 +58,9 @@ class NginxTlsSni01(common.TLSSNI01):
if vhosts and vhosts[0].addrs:
addresses.append(list(vhosts[0].addrs))
else:
# choose_vhosts might have modified vhosts, so put this after
ipv6, ipv6only = self.configurator.ipv6_info(
self.configurator.config.tls_sni_01_port)
if ipv6:
# If IPv6 is active in Nginx configuration
ipv6_addr = "[::]:{0} ssl".format(
@@ -141,6 +141,8 @@ class NginxTlsSni01(common.TLSSNI01):
with open(self.challenge_conf, "w") as new_conf:
nginxparser.dump(config, new_conf)
logger.debug("Generated server block:\n%s", str(config))
def _make_server_block(self, achall, addrs):
"""Creates a server block for a challenge.
+2 -2
View File
@@ -1,2 +1,2 @@
acme[dev]==0.25.0
-e .[dev]
acme[dev]==0.26.0
certbot[dev]==0.22.0
+2 -2
View File
@@ -2,12 +2,12 @@ from setuptools import setup
from setuptools import find_packages
version = '0.26.0.dev0'
version = '0.28.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme>=0.25.0',
'acme>=0.26.0',
'certbot>=0.22.0',
'mock',
'PyOpenSSL',
@@ -35,6 +35,7 @@ test_deployment_and_rollback() {
}
export default_server="default_server"
nginx -v
reload_nginx
certbot_test_nginx --domains nginx.wtf run
test_deployment_and_rollback nginx.wtf
@@ -253,7 +253,7 @@ class InstallerTest(certbot_test_util.ConfigTestCase):
fake_set.reset_mock()
installer.deploy_cert("example.com", "cert_path", "key_path",
"chain_path", "fullchain_path")
fake_set.assert_not_called()
self.assertFalse(fake_set.called)
@certbot_test_util.patch_get_utility()
def test_deploy_already_secure(self, mock_util):
+1 -1
View File
@@ -1,4 +1,4 @@
"""Certbot client."""
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
__version__ = '0.26.0.dev0'
__version__ = '0.28.0.dev0'
+57 -29
View File
@@ -1,5 +1,6 @@
"""Creates ACME accounts for server."""
import datetime
import functools
import hashlib
import logging
import os
@@ -16,6 +17,7 @@ import zope.component
from acme import fields as acme_fields
from acme import messages
from certbot import compat
from certbot import constants
from certbot import errors
from certbot import interfaces
@@ -139,7 +141,7 @@ class AccountFileStorage(interfaces.AccountStorage):
"""
def __init__(self, config):
self.config = config
util.make_or_verify_dir(config.accounts_dir, 0o700, os.geteuid(),
util.make_or_verify_dir(config.accounts_dir, 0o700, compat.os_geteuid(),
self.config.strict_permissions)
def _account_dir_path(self, account_id):
@@ -191,6 +193,11 @@ class AccountFileStorage(interfaces.AccountStorage):
def find_all(self):
return self._find_all_for_server_path(self.config.server_path)
def _symlink_to_account_dir(self, prev_server_path, server_path, account_id):
prev_account_dir = self._account_dir_path_for_server_path(account_id, prev_server_path)
new_account_dir = self._account_dir_path_for_server_path(account_id, server_path)
os.symlink(prev_account_dir, new_account_dir)
def _symlink_to_accounts_dir(self, prev_server_path, server_path):
accounts_dir = self.config.accounts_dir_for_server_path(server_path)
if os.path.islink(accounts_dir):
@@ -207,7 +214,12 @@ class AccountFileStorage(interfaces.AccountStorage):
prev_server_path = constants.LE_REUSE_SERVERS[server_path]
prev_loaded_account = self._load_for_server_path(account_id, prev_server_path)
# we didn't error so we found something, so create a symlink to that
self._symlink_to_accounts_dir(prev_server_path, server_path)
accounts_dir = self.config.accounts_dir_for_server_path(server_path)
# If accounts_dir isn't empty, make an account specific symlink
if os.listdir(accounts_dir):
self._symlink_to_account_dir(prev_server_path, server_path, account_id)
else:
self._symlink_to_accounts_dir(prev_server_path, server_path)
return prev_loaded_account
else:
raise errors.AccountNotFound(
@@ -250,53 +262,69 @@ class AccountFileStorage(interfaces.AccountStorage):
:param account_id: id of account which should be deleted
"""
# Step 1: remove the account itself
account_dir_path = self._account_dir_path(account_id)
if not os.path.isdir(account_dir_path):
raise errors.AccountNotFound(
"Account at %s does not exist" % account_dir_path)
shutil.rmtree(account_dir_path)
# Step 1: Delete account specific links and the directory
self._delete_account_dir_for_server_path(account_id, self.config.server_path)
# Step 2: remove the directory if it's empty, and linked directories
# Step 2: Remove any accounts links and directories that are now empty
if not os.listdir(self.config.accounts_dir):
self._delete_accounts_dir_for_server_path(self.config.server_path)
def _delete_account_dir_for_server_path(self, account_id, server_path):
link_func = functools.partial(self._account_dir_path_for_server_path, account_id)
nonsymlinked_dir = self._delete_links_and_find_target_dir(server_path, link_func)
shutil.rmtree(nonsymlinked_dir)
def _delete_accounts_dir_for_server_path(self, server_path):
accounts_dir_path = self.config.accounts_dir_for_server_path(server_path)
link_func = self.config.accounts_dir_for_server_path
nonsymlinked_dir = self._delete_links_and_find_target_dir(server_path, link_func)
os.rmdir(nonsymlinked_dir)
def _delete_links_and_find_target_dir(self, server_path, link_func):
"""Delete symlinks and return the nonsymlinked directory path.
:param str server_path: file path based on server
:param callable link_func: callable that returns possible links
given a server_path
:returns: the final, non-symlinked target
:rtype: str
"""
dir_path = link_func(server_path)
# does an appropriate directory link to me? if so, make sure that's gone
reused_servers = {}
for k in constants.LE_REUSE_SERVERS:
reused_servers[constants.LE_REUSE_SERVERS[k]] = k
# is there a next one up? call that and be done
if server_path in reused_servers:
next_server_path = reused_servers[server_path]
next_accounts_dir_path = self.config.accounts_dir_for_server_path(next_server_path)
if os.path.islink(next_accounts_dir_path) \
and os.readlink(next_accounts_dir_path) == accounts_dir_path:
self._delete_accounts_dir_for_server_path(next_server_path)
return
# is there a next one up?
possible_next_link = True
while possible_next_link:
possible_next_link = False
if server_path in reused_servers:
next_server_path = reused_servers[server_path]
next_dir_path = link_func(next_server_path)
if os.path.islink(next_dir_path) and os.readlink(next_dir_path) == dir_path:
possible_next_link = True
server_path = next_server_path
dir_path = next_dir_path
# if there's not a next one up to delete, then delete me
# and whatever I link to if applicable
if os.path.islink(accounts_dir_path):
# save my info then delete me
target = os.readlink(accounts_dir_path)
os.unlink(accounts_dir_path)
# then delete whatever I linked to, if appropriate
if server_path in constants.LE_REUSE_SERVERS:
prev_server_path = constants.LE_REUSE_SERVERS[server_path]
prev_accounts_dir_path = self.config.accounts_dir_for_server_path(prev_server_path)
if target == prev_accounts_dir_path:
self._delete_accounts_dir_for_server_path(prev_server_path)
else:
# just delete me
os.rmdir(accounts_dir_path)
# and whatever I link to
while os.path.islink(dir_path):
target = os.readlink(dir_path)
os.unlink(dir_path)
dir_path = target
return dir_path
def _save(self, account, acme, regr_only):
account_dir_path = self._account_dir_path(account.id)
util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(),
util.make_or_verify_dir(account_dir_path, 0o700, compat.os_geteuid(),
self.config.strict_permissions)
try:
with open(self._regr_path(account_dir_path), "w") as regr_file:
+4 -3
View File
@@ -8,6 +8,7 @@ import traceback
import zope.component
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from certbot import compat
from certbot import crypto_util
from certbot import errors
from certbot import interfaces
@@ -104,7 +105,7 @@ def lineage_for_certname(cli_config, certname):
"""Find a lineage object with name certname."""
configs_dir = cli_config.renewal_configs_dir
# Verify the directory is there
util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
util.make_or_verify_dir(configs_dir, mode=0o755, uid=compat.os_geteuid())
try:
renewal_file = storage.renewal_file_for_certname(cli_config, certname)
except errors.CertStorageError:
@@ -353,7 +354,7 @@ def _describe_certs(config, parsed_certs, parse_failures):
notify("Found the following {0}certs:".format(match))
notify(_report_human_readable(config, parsed_certs))
if parse_failures:
notify("\nThe following renewal configuration files "
notify("\nThe following renewal configurations "
"were invalid:")
notify(_report_lines(parse_failures))
@@ -374,7 +375,7 @@ def _search_lineages(cli_config, func, initial_rv, *args):
"""
configs_dir = cli_config.renewal_configs_dir
# Verify the directory is there
util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
util.make_or_verify_dir(configs_dir, mode=0o755, uid=compat.os_geteuid())
rv = initial_rv
for renewal_file in storage.renewal_conf_files(cli_config):
+5 -4
View File
@@ -96,7 +96,7 @@ obtain, install, and renew certificates:
manage certificates:
certificates Display information about certificates you have from Certbot
revoke Revoke a certificate (supply --cert-path)
revoke Revoke a certificate (supply --cert-path or --cert-name)
delete Delete a certificate
manage your account with Let's Encrypt:
@@ -387,9 +387,10 @@ VERB_HELP = [
"usage": "\n\n certbot delete --cert-name CERTNAME\n\n"
}),
("revoke", {
"short": "Revoke a certificate specified with --cert-path",
"short": "Revoke a certificate specified with --cert-path or --cert-name",
"opts": "Options for revocation of certificates",
"usage": "\n\n certbot revoke --cert-path /path/to/fullchain.pem [options]\n\n"
"usage": "\n\n certbot revoke [--cert-path /path/to/fullchain.pem | "
"--cert-name example.com] [options]\n\n"
}),
("register", {
"short": "Register for account with Let's Encrypt / other ACME server",
@@ -1333,7 +1334,7 @@ def _paths_parser(helpful):
add(sections, "--cert-path", type=os.path.abspath,
default=flag_default("auth_cert_path"), help=cph)
elif verb == "revoke":
add(sections, "--cert-path", type=read_file, required=True, help=cph)
add(sections, "--cert-path", type=read_file, required=False, help=cph)
else:
add(sections, "--cert-path", type=os.path.abspath, help=cph)
+2 -1
View File
@@ -24,6 +24,7 @@ import certbot
from certbot import account
from certbot import auth_handler
from certbot import cli
from certbot import compat
from certbot import constants
from certbot import crypto_util
from certbot import eff
@@ -447,7 +448,7 @@ class Client(object):
"""
for path in cert_path, chain_path, fullchain_path:
util.make_or_verify_dir(
os.path.dirname(path), 0o755, os.geteuid(),
os.path.dirname(path), 0o755, compat.os_geteuid(),
self.config.strict_permissions)
+150
View File
@@ -0,0 +1,150 @@
"""
Compatibility layer to run certbot both on Linux and Windows.
The approach used here is similar to Modernizr for Web browsers.
We do not check the platform type to determine if a particular logic is supported.
Instead, we apply a logic, and then fallback to another logic if first logic
is not supported at runtime.
Then logic chains are abstracted into single functions to be exposed to certbot.
"""
import os
import select
import sys
import errno
import ctypes
import stat
from certbot import errors
try:
# Linux specific
import fcntl # pylint: disable=import-error
except ImportError:
# Windows specific
import msvcrt # pylint: disable=import-error
UNPRIVILEGED_SUBCOMMANDS_ALLOWED = [
'certificates', 'enhance', 'revoke', 'delete',
'register', 'unregister', 'config_changes', 'plugins']
def raise_for_non_administrative_windows_rights(subcommand):
"""
On Windows, raise if current shell does not have the administrative rights.
Do nothing on Linux.
:param str subcommand: The subcommand (like 'certonly') passed to the certbot client.
:raises .errors.Error: If the provided subcommand must be run on a shell with
administrative rights, and current shell does not have these rights.
"""
# Why not simply try ctypes.windll.shell32.IsUserAnAdmin() and catch AttributeError ?
# Because windll exists only on a Windows runtime, and static code analysis engines
# do not like at all non existent objects when run from Linux (even if we handle properly
# all the cases in the code).
# So we access windll only by reflection to trick theses engines.
if hasattr(ctypes, 'windll') and subcommand not in UNPRIVILEGED_SUBCOMMANDS_ALLOWED:
windll = getattr(ctypes, 'windll')
if windll.shell32.IsUserAnAdmin() == 0:
raise errors.Error(
'Error, "{0}" subcommand must be run on a shell with administrative rights.'
.format(subcommand))
def os_geteuid():
"""
Get current user uid
:returns: The current user uid.
:rtype: int
"""
try:
# Linux specific
return os.geteuid()
except AttributeError:
# Windows specific
return 0
def readline_with_timeout(timeout, prompt):
"""
Read user input to return the first line entered, or raise after specified timeout.
:param float timeout: The timeout in seconds given to the user.
:param str prompt: The prompt message to display to the user.
:returns: The first line entered by the user.
:rtype: str
"""
try:
# Linux specific
#
# Call to select can only be done like this on UNIX
rlist, _, _ = select.select([sys.stdin], [], [], timeout)
if not rlist:
raise errors.Error(
"Timed out waiting for answer to prompt '{0}'".format(prompt))
return rlist[0].readline()
except OSError:
# Windows specific
#
# No way with select to make a timeout to the user input on Windows,
# as select only supports socket in this case.
# So no timeout on Windows for now.
return sys.stdin.readline()
def lock_file(fd):
"""
Lock the file linked to the specified file descriptor.
:param int fd: The file descriptor of the file to lock.
"""
if 'fcntl' in sys.modules:
# Linux specific
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
else:
# Windows specific
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
def release_locked_file(fd, path):
"""
Remove, close, and release a lock file specified by its file descriptor and its path.
:param int fd: The file descriptor of the lock file.
:param str path: The path of the lock file.
"""
# Linux specific
#
# It is important the lock file is removed before it's released,
# otherwise:
#
# process A: open lock file
# process B: release lock file
# process A: lock file
# process A: check device and inode
# process B: delete file
# process C: open and lock a different file at the same path
try:
os.remove(path)
except OSError as err:
if err.errno == errno.EACCES:
# Windows specific
# We will not be able to remove a file before closing it.
# To avoid race conditions described for Linux, we will not delete the lockfile,
# just close it to be reused on the next Certbot call.
pass
else:
raise
finally:
os.close(fd)
def compare_file_modes(mode1, mode2):
"""Return true if the two modes can be considered as equals for this platform"""
if 'fcntl' in sys.modules:
# Linux specific: standard compare
return oct(stat.S_IMODE(mode1)) == oct(stat.S_IMODE(mode2))
# Windows specific: most of mode bits are ignored on Windows. Only check user R/W rights.
return (stat.S_IMODE(mode1) & stat.S_IREAD == stat.S_IMODE(mode2) & stat.S_IREAD
and stat.S_IMODE(mode1) & stat.S_IWRITE == stat.S_IMODE(mode2) & stat.S_IWRITE)
+8 -4
View File
@@ -25,6 +25,7 @@ from OpenSSL import SSL # type: ignore
from acme import crypto_util as acme_crypto_util
from acme.magic_typing import IO # pylint: disable=unused-import, no-name-in-module
from certbot import compat
from certbot import errors
from certbot import interfaces
from certbot import util
@@ -60,7 +61,7 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"):
config = zope.component.getUtility(interfaces.IConfig)
# Save file
util.make_or_verify_dir(key_dir, 0o700, os.geteuid(),
util.make_or_verify_dir(key_dir, 0o700, compat.os_geteuid(),
config.strict_permissions)
key_f, key_path = util.unique_file(
os.path.join(key_dir, keyname), 0o600, "wb")
@@ -91,7 +92,7 @@ def init_save_csr(privkey, names, path):
privkey.pem, names, must_staple=config.must_staple)
# Save CSR
util.make_or_verify_dir(path, 0o755, os.geteuid(),
util.make_or_verify_dir(path, 0o755, compat.os_geteuid(),
config.strict_permissions)
csr_f, csr_filename = util.unique_file(
os.path.join(path, "csr-certbot.pem"), 0o644, "wb")
@@ -448,14 +449,17 @@ def _notAfterBefore(cert_path, method):
def sha256sum(filename):
"""Compute a sha256sum of a file.
NB: In given file, platform specific newlines characters will be converted
into their equivalent unicode counterparts before calculating the hash.
:param str filename: path to the file whose hash will be computed
:returns: sha256 digest of the file in hexadecimal
:rtype: str
"""
sha256 = hashlib.sha256()
with open(filename, 'rb') as f:
sha256.update(f.read())
with open(filename, 'rU') as file_d:
sha256.update(file_d.read().encode('UTF-8'))
return sha256.hexdigest()
def cert_and_chain_from_fullchain(fullchain_pem):
+2 -2
View File
@@ -49,9 +49,9 @@ class Completer(object):
readline.set_completer(self.complete)
readline.set_completer_delims(' \t\n;')
# readline can be implemented using GNU readline or libedit
# readline can be implemented using GNU readline, pyreadline or libedit
# which have different configuration syntax
if 'libedit' in readline.__doc__:
if readline.__doc__ is not None and 'libedit' in readline.__doc__:
readline.parse_and_bind('bind ^I rl_complete')
else:
readline.parse_and_bind('tab: complete')
+3 -8
View File
@@ -1,12 +1,12 @@
"""Certbot display."""
import logging
import os
import select
import sys
import textwrap
import zope.interface
from certbot import compat
from certbot import constants
from certbot import interfaces
from certbot import errors
@@ -53,7 +53,7 @@ def _wrap_lines(msg):
break_long_words=False,
break_on_hyphens=False))
return os.linesep.join(fixed_l)
return '\n'.join(fixed_l)
def input_with_timeout(prompt=None, timeout=36000.0):
@@ -79,13 +79,8 @@ def input_with_timeout(prompt=None, timeout=36000.0):
sys.stdout.write(prompt)
sys.stdout.flush()
# select can only be used like this on UNIX
rlist, _, _ = select.select([sys.stdin], [], [], timeout)
if not rlist:
raise errors.Error(
"Timed out waiting for answer to prompt '{0}'".format(prompt))
line = compat.readline_with_timeout(timeout, prompt)
line = rlist[0].readline()
if not line:
raise EOFError
return line.rstrip('\n')
+4 -19
View File
@@ -1,9 +1,9 @@
"""Implements file locks for locking files and directories in UNIX."""
import errno
import fcntl
import logging
import os
from certbot import compat
from certbot import errors
logger = logging.getLogger(__name__)
@@ -74,7 +74,7 @@ class LockFile(object):
"""
try:
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
compat.lock_file(fd)
except IOError as err:
if err.errno in (errno.EACCES, errno.EAGAIN):
logger.debug(
@@ -118,22 +118,7 @@ class LockFile(object):
def release(self):
"""Remove, close, and release the lock file."""
# It is important the lock file is removed before it's released,
# otherwise:
#
# process A: open lock file
# process B: release lock file
# process A: lock file
# process A: check device and inode
# process B: delete file
# process C: open and lock a different file at the same path
#
# Calling os.remove on a file that's in use doesn't work on
# Windows, but neither does locking with fcntl.
try:
os.remove(self._path)
compat.release_locked_file(self._fd, self._path)
finally:
try:
os.close(self._fd)
finally:
self._fd = None
self._fd = None
+2 -1
View File
@@ -23,6 +23,7 @@ import traceback
from acme import messages
from certbot import compat
from certbot import constants
from certbot import errors
from certbot import util
@@ -133,7 +134,7 @@ def setup_log_file_handler(config, logfile, fmt):
# TODO: logs might contain sensitive data such as contents of the
# private key! #525
util.set_up_core_dir(
config.logs_dir, 0o700, os.geteuid(), config.strict_permissions)
config.logs_dir, 0o700, compat.os_geteuid(), config.strict_permissions)
log_file_path = os.path.join(config.logs_dir, logfile)
try:
handler = logging.handlers.RotatingFileHandler(
+24 -48
View File
@@ -19,6 +19,7 @@ from certbot import account
from certbot import cert_manager
from certbot import cli
from certbot import client
from certbot import compat
from certbot import configuration
from certbot import constants
from certbot import crypto_util
@@ -531,8 +532,7 @@ def _determine_account(config):
def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-branches
"""Does the user want to delete their now-revoked certs? If run in non-interactive mode,
deleting happens automatically, unless if both `--cert-name` and `--cert-path` were
specified with conflicting values.
deleting happens automatically.
:param config: parsed command line arguments
:type config: interfaces.IConfig
@@ -556,50 +556,13 @@ def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-b
reporter_util.add_message("Not deleting revoked certs.", reporter_util.LOW_PRIORITY)
return
if not (config.certname or config.cert_path):
raise errors.Error('At least one of --cert-path or --cert-name must be specified.')
# config.cert_path must have been set
# config.certname may have been set
assert config.cert_path
if config.certname and config.cert_path:
# first, check if certname and cert_path imply the same certs
implied_cert_name = cert_manager.cert_path_to_lineage(config)
if implied_cert_name != config.certname:
cert_path_implied_cert_name = cert_manager.cert_path_to_lineage(config)
cert_path_implied_conf = storage.renewal_file_for_certname(config,
cert_path_implied_cert_name)
cert_path_cert = storage.RenewableCert(cert_path_implied_conf, config)
cert_path_info = cert_manager.human_readable_cert_info(config, cert_path_cert,
skip_filter_checks=True)
cert_name_implied_conf = storage.renewal_file_for_certname(config, config.certname)
cert_name_cert = storage.RenewableCert(cert_name_implied_conf, config)
cert_name_info = cert_manager.human_readable_cert_info(config, cert_name_cert)
msg = ("You specified conflicting values for --cert-path and --cert-name. "
"Which did you mean to select?")
choices = [cert_path_info, cert_name_info]
try:
code, index = display.menu(msg,
choices, ok_label="Select", force_interactive=True)
except errors.MissingCommandlineFlag:
error_msg = ('To run in non-interactive mode, you must either specify only one of '
'--cert-path or --cert-name, or both must point to the same certificate lineages.')
raise errors.Error(error_msg)
if code != display_util.OK or not index in range(0, len(choices)):
raise errors.Error("User ended interaction.")
if index == 0:
config.certname = cert_path_implied_cert_name
else:
config.cert_path = storage.cert_path_for_cert_name(config, config.certname)
elif config.cert_path:
if not config.certname:
config.certname = cert_manager.cert_path_to_lineage(config)
else: # if only config.certname was specified
config.cert_path = storage.cert_path_for_cert_name(config, config.certname)
# don't delete if the archive_dir is used by some other lineage
archive_dir = storage.full_archive_path(
configobj.ConfigObj(storage.renewal_file_for_certname(config, config.certname)),
@@ -1065,6 +1028,14 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config
"""
# For user-agent construction
config.installer = config.authenticator = None
if config.cert_path is None and config.certname:
config.cert_path = storage.cert_path_for_cert_name(config, config.certname)
elif not config.cert_path or (config.cert_path and config.certname):
# intentionally not supporting --cert-path & --cert-name together,
# to avoid dealing with mismatched values
raise errors.Error("Error! Exactly one of --cert-path or --cert-name must be specified!")
if config.key_path is not None: # revocation by cert key
logger.debug("Revoking %s using cert key %s",
config.cert_path[0], config.key_path[0])
@@ -1077,7 +1048,6 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config
acme = client.acme_from_config_key(config, acc.key, acc.regr)
cert = crypto_util.pyopenssl_load_certificate(config.cert_path[1])[0]
logger.debug("Reason code for revocation: %s", config.reason)
try:
acme.revoke(jose.ComparableX509(cert), config.reason)
_delete_if_appropriate(config)
@@ -1165,7 +1135,8 @@ def _csr_get_and_save_cert(config, le_client):
"Dry run: skipping saving certificate to %s", config.cert_path)
return None, None
cert_path, _, fullchain_path = le_client.save_certificate(
cert, chain, config.cert_path, config.chain_path, config.fullchain_path)
cert, chain, os.path.normpath(config.cert_path),
os.path.normpath(config.chain_path), os.path.normpath(config.fullchain_path))
return cert_path, fullchain_path
def renew_cert(config, plugins, lineage):
@@ -1289,16 +1260,16 @@ def make_or_verify_needed_dirs(config):
"""
util.set_up_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE,
os.geteuid(), config.strict_permissions)
compat.os_geteuid(), config.strict_permissions)
util.set_up_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE,
os.geteuid(), config.strict_permissions)
compat.os_geteuid(), config.strict_permissions)
hook_dirs = (config.renewal_pre_hooks_dir,
config.renewal_deploy_hooks_dir,
config.renewal_post_hooks_dir,)
for hook_dir in hook_dirs:
util.make_or_verify_dir(hook_dir,
uid=os.geteuid(),
uid=compat.os_geteuid(),
strict=config.strict_permissions)
@@ -1333,6 +1304,7 @@ def main(cli_args=sys.argv[1:]):
:raises errors.Error: error if plugin command is not supported
"""
log.pre_arg_parse_setup()
plugins = plugins_disco.PluginsRegistry.find_all()
@@ -1346,6 +1318,10 @@ def main(cli_args=sys.argv[1:]):
config = configuration.NamespaceConfig(args)
zope.component.provideUtility(config)
# On windows, shell without administrative right cannot create symlinks required by certbot.
# So we check the rights before continuing.
compat.raise_for_non_administrative_windows_rights(config.verb)
try:
log.post_arg_parse_setup(config)
make_or_verify_needed_dirs(config)
+21
View File
@@ -94,6 +94,16 @@ using the secret key
{key}
when it receives a TLS ClientHello with the SNI extension set to
{sni_domain}
"""
_SUBSEQUENT_CHALLENGE_INSTRUCTIONS = """
(This must be set up in addition to the previous challenges; do not remove,
replace, or undo the previous challenge tasks yet.)
"""
_SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS = """
(This must be set up in addition to the previous challenges; do not remove,
replace, or undo the previous challenge tasks yet. Note that you might be
asked to create multiple distinct TXT records with the same name. This is
permitted by DNS standards.)
"""
def __init__(self, *args, **kwargs):
@@ -103,6 +113,8 @@ when it receives a TLS ClientHello with the SNI extension set to
self.env = dict() \
# type: Dict[achallenges.KeyAuthorizationAnnotatedChallenge, Dict[str, str]]
self.tls_sni_01 = None
self.subsequent_dns_challenge = False
self.subsequent_any_challenge = False
@classmethod
def add_parser_arguments(cls, add):
@@ -212,8 +224,17 @@ when it receives a TLS ClientHello with the SNI extension set to
key=self.tls_sni_01.get_key_path(achall),
port=self.config.tls_sni_01_port,
sni_domain=self.tls_sni_01.get_z_domain(achall))
if isinstance(achall.chall, challenges.DNS01):
if self.subsequent_dns_challenge:
# 2nd or later dns-01 challenge
msg += self._SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS
self.subsequent_dns_challenge = True
elif self.subsequent_any_challenge:
# 2nd or later challenge of another type
msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS
display = zope.component.getUtility(interfaces.IDisplay)
display.notification(msg, wrap=False, force_interactive=True)
self.subsequent_any_challenge = True
def cleanup(self, achalls): # pylint: disable=missing-docstring
if self.conf('cleanup-hook'):
+12 -7
View File
@@ -4,6 +4,7 @@ import unittest
import six
import mock
import sys
from acme import challenges
@@ -20,8 +21,9 @@ class AuthenticatorTest(test_util.TempDirTestCase):
super(AuthenticatorTest, self).setUp()
self.http_achall = acme_util.HTTP01_A
self.dns_achall = acme_util.DNS01_A
self.dns_achall_2 = acme_util.DNS01_A_2
self.tls_sni_achall = acme_util.TLSSNI01_A
self.achalls = [self.http_achall, self.dns_achall, self.tls_sni_achall]
self.achalls = [self.http_achall, self.dns_achall, self.tls_sni_achall, self.dns_achall_2]
for d in ["config_dir", "work_dir", "in_progress"]:
os.mkdir(os.path.join(self.tempdir, d))
# "backup_dir" and "temp_checkpoint_dir" get created in
@@ -74,12 +76,14 @@ class AuthenticatorTest(test_util.TempDirTestCase):
def test_script_perform(self):
self.config.manual_public_ip_logging_ok = True
self.config.manual_auth_hook = (
'echo ${CERTBOT_DOMAIN}; '
'echo ${CERTBOT_TOKEN:-notoken}; '
'echo ${CERTBOT_CERT_PATH:-nocert}; '
'echo ${CERTBOT_KEY_PATH:-nokey}; '
'echo ${CERTBOT_SNI_DOMAIN:-nosnidomain}; '
'echo ${CERTBOT_VALIDATION:-novalidation};')
'{0} -c "from __future__ import print_function;'
'import os; print(os.environ.get(\'CERTBOT_DOMAIN\'));'
'print(os.environ.get(\'CERTBOT_TOKEN\', \'notoken\'));'
'print(os.environ.get(\'CERTBOT_CERT_PATH\', \'nocert\'));'
'print(os.environ.get(\'CERTBOT_KEY_PATH\', \'nokey\'));'
'print(os.environ.get(\'CERTBOT_SNI_DOMAIN\', \'nosnidomain\'));'
'print(os.environ.get(\'CERTBOT_VALIDATION\', \'novalidation\'));"'
.format(sys.executable))
dns_expected = '{0}\n{1}\n{2}\n{3}\n{4}\n{5}'.format(
self.dns_achall.domain, 'notoken',
'nocert', 'nokey', 'nosnidomain',
@@ -127,6 +131,7 @@ class AuthenticatorTest(test_util.TempDirTestCase):
achall.validation(achall.account_key) in args[0])
self.assertFalse(kwargs['wrap'])
@test_util.broken_on_windows
def test_cleanup(self):
self.config.manual_public_ip_logging_ok = True
self.config.manual_auth_hook = 'echo foo;'
+6 -5
View File
@@ -9,18 +9,19 @@ logger = logging.getLogger(__name__)
def get_prefixes(path):
"""Retrieves all possible path prefixes of a path, in descending order
of length. For instance,
/a/b/c/ => ['/a/b/c/', '/a/b/c', '/a/b', '/a', '/']
(linux) /a/b/c returns ['/a/b/c', '/a/b', '/a', '/']
(windows) C:\\a\\b\\c returns ['C:\\a\\b\\c', 'C:\\a\\b', 'C:\\a', 'C:']
:param str path: the path to break into prefixes
:returns: all possible path prefixes of given path in descending order
:rtype: `list` of `str`
"""
prefix = path
prefix = os.path.normpath(path)
prefixes = []
while len(prefix) > 0:
prefixes.append(prefix)
prefix, _ = os.path.split(prefix)
# break once we hit '/'
# break once we hit the root path
if prefix == prefixes[-1]:
break
return prefixes
@@ -51,6 +52,6 @@ def path_surgery(cmd):
return True
else:
expanded = " expanded" if any(added) else ""
logger.warning("Failed to find executable %s in%s PATH: %s", cmd,
expanded, path)
logger.debug("Failed to find executable %s in%s PATH: %s", cmd,
expanded, path)
return False
+8 -10
View File
@@ -4,21 +4,21 @@ import unittest
import mock
class GetPrefixTest(unittest.TestCase):
"""Tests for certbot.plugins.get_prefixes."""
def test_get_prefix(self):
from certbot.plugins.util import get_prefixes
self.assertEqual(get_prefixes("/a/b/c/"), ['/a/b/c/', '/a/b/c', '/a/b', '/a', '/'])
self.assertEqual(get_prefixes("/"), ["/"])
self.assertEqual(get_prefixes("a"), ["a"])
self.assertEqual(
get_prefixes('/a/b/c'),
[os.path.normpath(path) for path in ['/a/b/c', '/a/b', '/a', '/']])
self.assertEqual(get_prefixes('/'), [os.path.normpath('/')])
self.assertEqual(get_prefixes('a'), ['a'])
class PathSurgeryTest(unittest.TestCase):
"""Tests for certbot.plugins.path_surgery."""
@mock.patch("certbot.plugins.util.logger.warning")
@mock.patch("certbot.plugins.util.logger.debug")
def test_path_surgery(self, mock_debug, mock_warn):
def test_path_surgery(self, mock_debug):
from certbot.plugins.util import path_surgery
all_path = {"PATH": "/usr/local/bin:/bin/:/usr/sbin/:/usr/local/sbin/"}
with mock.patch.dict('os.environ', all_path):
@@ -26,14 +26,12 @@ class PathSurgeryTest(unittest.TestCase):
mock_exists.return_value = True
self.assertEqual(path_surgery("eg"), True)
self.assertEqual(mock_debug.call_count, 0)
self.assertEqual(mock_warn.call_count, 0)
self.assertEqual(os.environ["PATH"], all_path["PATH"])
no_path = {"PATH": "/tmp/"}
with mock.patch.dict('os.environ', no_path):
path_surgery("thingy")
self.assertEqual(mock_debug.call_count, 1)
self.assertEqual(mock_warn.call_count, 1)
self.assertTrue("Failed to find" in mock_warn.call_args[0][0])
self.assertEqual(mock_debug.call_count, 2)
self.assertTrue("Failed to find" in mock_debug.call_args[0][0])
self.assertTrue("/usr/local/bin" in os.environ["PATH"])
self.assertTrue("/tmp" in os.environ["PATH"])
+4 -2
View File
@@ -170,7 +170,9 @@ to serve all files under specified web root ({0})."""
old_umask = os.umask(0o022)
try:
stat_path = os.stat(path)
for prefix in sorted(util.get_prefixes(self.full_roots[name]), key=len):
# We ignore the last prefix in the next iteration,
# as it does not correspond to a folder path ('/' or 'C:')
for prefix in sorted(util.get_prefixes(self.full_roots[name])[:-1], key=len):
try:
# This is coupled with the "umask" call above because
# os.mkdir's "mode" parameter may not always work:
@@ -180,7 +182,7 @@ to serve all files under specified web root ({0})."""
# Set owner as parent directory if possible
try:
os.chown(prefix, stat_path.st_uid, stat_path.st_gid)
except OSError as exception:
except (OSError, AttributeError) as exception:
logger.info("Unable to change owner and uid of webroot directory")
logger.debug("Error was: %s", exception)
except OSError as exception:
+6 -6
View File
@@ -4,9 +4,9 @@ from __future__ import print_function
import argparse
import errno
import json
import os
import shutil
import stat
import tempfile
import unittest
@@ -17,6 +17,7 @@ import six
from acme import challenges
from certbot import achallenges
from certbot import compat
from certbot import errors
from certbot.display import util as display_util
@@ -142,6 +143,7 @@ class AuthenticatorTest(unittest.TestCase):
self.assertRaises(errors.PluginError, self.auth.perform, [])
os.chmod(self.path, 0o700)
@test_util.skip_on_windows('On Windows, there is no chown.')
@mock.patch("certbot.plugins.webroot.os.chown")
def test_failed_chown(self, mock_chown):
mock_chown.side_effect = OSError(errno.EACCES, "msg")
@@ -169,16 +171,14 @@ class AuthenticatorTest(unittest.TestCase):
# Remove exec bit from permission check, so that it
# matches the file
self.auth.perform([self.achall])
path_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode)
self.assertEqual(path_permissions, 0o644)
self.assertTrue(compat.compare_file_modes(os.stat(self.validation_path).st_mode, 0o644))
# Check permissions of the directories
for dirpath, dirnames, _ in os.walk(self.path):
for directory in dirnames:
full_path = os.path.join(dirpath, directory)
dir_permissions = stat.S_IMODE(os.stat(full_path).st_mode)
self.assertEqual(dir_permissions, 0o755)
self.assertTrue(compat.compare_file_modes(os.stat(full_path).st_mode, 0o755))
parent_gid = os.stat(self.path).st_gid
parent_uid = os.stat(self.path).st_uid
@@ -274,7 +274,7 @@ class WebrootActionTest(unittest.TestCase):
def test_webroot_map_action(self):
args = self.parser.parse_args(
["--webroot-map", '{{"thing.com":"{0}"}}'.format(self.path)])
["--webroot-map", json.dumps({'thing.com': self.path})])
self.assertEqual(args.webroot_map["thing.com"], self.path)
def test_domain_before_webroot(self):
+2 -2
View File
@@ -301,7 +301,7 @@ def renew_cert(config, domains, le_client, lineage):
domains = lineage.names()
# The private key is the existing lineage private key if reuse_key is set.
# Otherwise, generate a fresh private key by passing None.
new_key = lineage.privkey if config.reuse_key else None
new_key = os.path.normpath(lineage.privkey) if config.reuse_key else None
new_cert, new_chain, new_key, _ = le_client.obtain_certificate(domains, new_key)
if config.dry_run:
logger.debug("Dry run: skipping updating lineage at %s",
@@ -359,7 +359,7 @@ def _renew_describe_results(config, renew_successes, renew_failures,
notify_error(report(renew_failures, "failure"))
if parse_failures:
notify("\nAdditionally, the following renewal configuration files "
notify("\nAdditionally, the following renewal configurations "
"were invalid: ")
notify(report(parse_failures, "parsefail"))
+4 -3
View File
@@ -10,6 +10,7 @@ import traceback
import six
import zope.component
from certbot import compat
from certbot import constants
from certbot import errors
from certbot import interfaces
@@ -65,7 +66,7 @@ class Reverter(object):
self.config = config
util.make_or_verify_dir(
config.backup_dir, constants.CONFIG_DIRS_MODE, os.geteuid(),
config.backup_dir, constants.CONFIG_DIRS_MODE, compat.os_geteuid(),
self.config.strict_permissions)
def revert_temporary_config(self):
@@ -219,7 +220,7 @@ class Reverter(object):
"""
util.make_or_verify_dir(
cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(),
cp_dir, constants.CONFIG_DIRS_MODE, compat.os_geteuid(),
self.config.strict_permissions)
op_fd, existing_filepaths = self._read_and_append(
@@ -433,7 +434,7 @@ class Reverter(object):
cp_dir = self.config.in_progress_dir
util.make_or_verify_dir(
cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(),
cp_dir, constants.CONFIG_DIRS_MODE, compat.os_geteuid(),
self.config.strict_permissions)
return cp_dir
+24 -15
View File
@@ -214,6 +214,26 @@ def get_link_target(link):
target = os.path.join(os.path.dirname(link), target)
return os.path.abspath(target)
def _write_live_readme_to(readme_path, is_base_dir=False):
prefix = ""
if is_base_dir:
prefix = "[cert name]/"
with open(readme_path, "w") as f:
logger.debug("Writing README to %s.", readme_path)
f.write("This directory contains your keys and certificates.\n\n"
"`{prefix}privkey.pem` : the private key for your certificate.\n"
"`{prefix}fullchain.pem`: the certificate file used in most server software.\n"
"`{prefix}chain.pem` : used for OCSP stapling in Nginx >=1.3.7.\n"
"`{prefix}cert.pem` : will break many server configurations, and "
"should not be used\n"
" without reading further documentation (see link below).\n\n"
"WARNING: DO NOT MOVE OR RENAME THESE FILES!\n"
" Certbot expects these files to remain in this location in order\n"
" to function properly!\n\n"
"We recommend not moving these files. For more information, see the Certbot\n"
"User Guide at https://certbot.eff.org/docs/using.html#where-are-my-"
"certificates.\n".format(prefix=prefix))
def _relevant(option):
"""
@@ -1003,6 +1023,9 @@ class RenewableCert(object):
logger.debug("Creating directory %s.", i)
config_file, config_filename = util.unique_lineage_name(
cli_config.renewal_configs_dir, lineagename)
base_readme_path = os.path.join(cli_config.live_dir, README)
if not os.path.exists(base_readme_path):
_write_live_readme_to(base_readme_path, is_base_dir=True)
# Determine where on disk everything will go
# lineagename will now potentially be modified based on which
@@ -1045,21 +1068,7 @@ class RenewableCert(object):
# Write a README file to the live directory
readme_path = os.path.join(live_dir, README)
with open(readme_path, "w") as f:
logger.debug("Writing README to %s.", readme_path)
f.write("This directory contains your keys and certificates.\n\n"
"`privkey.pem` : the private key for your certificate.\n"
"`fullchain.pem`: the certificate file used in most server software.\n"
"`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.\n"
"`cert.pem` : will break many server configurations, and "
"should not be used\n"
" without reading further documentation (see link below).\n\n"
"WARNING: DO NOT MOVE THESE FILES!\n"
" Certbot expects these files to remain in this location in order\n"
" to function properly!\n\n"
"We recommend not moving these files. For more information, see the Certbot\n"
"User Guide at https://certbot.eff.org/docs/using.html#where-are-my-"
"certificates.\n")
_write_live_readme_to(readme_path)
# Document what we've done in a new renewal config file
config_file.close()
+31 -1
View File
@@ -116,6 +116,7 @@ class AccountFileStorageTest(test_util.ConfigTestCase):
def test_init_creates_dir(self):
self.assertTrue(os.path.isdir(self.config.accounts_dir))
@test_util.broken_on_windows
def test_save_and_restore(self):
self.storage.save(self.acc, self.mock_client)
account_path = os.path.join(self.config.accounts_dir, self.acc.id)
@@ -218,12 +219,14 @@ class AccountFileStorageTest(test_util.ConfigTestCase):
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
self.assertEqual([], self.storage.find_all())
@test_util.broken_on_windows
def test_upgrade_version_staging(self):
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
self.storage.save(self.acc, self.mock_client)
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
self.assertEqual([self.acc], self.storage.find_all())
@test_util.broken_on_windows
def test_upgrade_version_production(self):
self._set_server('https://acme-v01.api.letsencrypt.org/directory')
self.storage.save(self.acc, self.mock_client)
@@ -241,6 +244,7 @@ class AccountFileStorageTest(test_util.ConfigTestCase):
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
self.assertEqual([], self.storage.find_all())
@test_util.broken_on_windows
def test_upgrade_load(self):
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
self.storage.save(self.acc, self.mock_client)
@@ -249,6 +253,15 @@ class AccountFileStorageTest(test_util.ConfigTestCase):
account = self.storage.load(self.acc.id)
self.assertEqual(prev_account, account)
@test_util.broken_on_windows
def test_upgrade_load_single_account(self):
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
self.storage.save(self.acc, self.mock_client)
prev_account = self.storage.load(self.acc.id)
self._set_server_and_stop_symlink('https://acme-staging-v02.api.letsencrypt.org/directory')
account = self.storage.load(self.acc.id)
self.assertEqual(prev_account, account)
def test_load_ioerror(self):
self.storage.save(self.acc, self.mock_client)
mock_open = mock.mock_open()
@@ -265,6 +278,7 @@ class AccountFileStorageTest(test_util.ConfigTestCase):
errors.AccountStorageError, self.storage.save,
self.acc, self.mock_client)
@test_util.broken_on_windows
def test_delete(self):
self.storage.save(self.acc, self.mock_client)
self.storage.delete(self.acc.id)
@@ -287,7 +301,7 @@ class AccountFileStorageTest(test_util.ConfigTestCase):
self._set_server('https://acme-staging.api.letsencrypt.org/directory')
self.storage.save(self.acc, self.mock_client)
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
self.storage.find_all()
self.storage.load(self.acc.id)
# delete starting at given server_url
self._set_server(server_url)
@@ -299,14 +313,30 @@ class AccountFileStorageTest(test_util.ConfigTestCase):
self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')
self.assertRaises(errors.AccountNotFound, self.storage.load, self.acc.id)
@test_util.broken_on_windows
def test_delete_folders_up(self):
self._test_delete_folders('https://acme-staging.api.letsencrypt.org/directory')
self._assert_symlinked_account_removed()
@test_util.broken_on_windows
def test_delete_folders_down(self):
self._test_delete_folders('https://acme-staging-v02.api.letsencrypt.org/directory')
self._assert_symlinked_account_removed()
def _set_server_and_stop_symlink(self, server_path):
self._set_server(server_path)
with open(os.path.join(self.config.accounts_dir, 'foo'), 'w') as f:
f.write('bar')
@test_util.broken_on_windows
def test_delete_shared_account_up(self):
self._set_server_and_stop_symlink('https://acme-staging-v02.api.letsencrypt.org/directory')
self._test_delete_folders('https://acme-staging.api.letsencrypt.org/directory')
@test_util.broken_on_windows
def test_delete_shared_account_down(self):
self._set_server_and_stop_symlink('https://acme-staging-v02.api.letsencrypt.org/directory')
self._test_delete_folders('https://acme-staging-v02.api.letsencrypt.org/directory')
if __name__ == "__main__":
unittest.main() # pragma: no cover
+3
View File
@@ -21,6 +21,7 @@ HTTP01 = challenges.HTTP01(
TLSSNI01 = challenges.TLSSNI01(
token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA"))
DNS01 = challenges.DNS01(token=b"17817c66b60ce2e4012dfad92657527a")
DNS01_2 = challenges.DNS01(token=b"cafecafecafecafecafecafe0feedbac")
CHALLENGES = [HTTP01, TLSSNI01, DNS01]
@@ -49,6 +50,7 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name
TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING)
HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING)
DNS01_P = chall_to_challb(DNS01, messages.STATUS_PENDING)
DNS01_P_2 = chall_to_challb(DNS01_2, messages.STATUS_PENDING)
CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS01_P]
@@ -57,6 +59,7 @@ CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS01_P]
HTTP01_A = auth_handler.challb_to_achall(HTTP01_P, JWK, "example.com")
TLSSNI01_A = auth_handler.challb_to_achall(TLSSNI01_P, JWK, "example.net")
DNS01_A = auth_handler.challb_to_achall(DNS01_P, JWK, "example.org")
DNS01_A_2 = auth_handler.challb_to_achall(DNS01_P_2, JWK, "esimerkki.example.org")
ACHALLENGES = [HTTP01_A, TLSSNI01_A, DNS01_A]
+2 -2
View File
@@ -204,7 +204,7 @@ class CertificatesTest(BaseCertManagerTest):
shutil.rmtree(empty_tempdir)
@mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_revoked')
def test_report_human_readable(self, mock_revoked):
def test_report_human_readable(self, mock_revoked): #pylint: disable=too-many-statements
mock_revoked.return_value = None
from certbot import cert_manager
import datetime, pytz
@@ -228,7 +228,7 @@ class CertificatesTest(BaseCertManagerTest):
cert.target_expiry += datetime.timedelta(hours=2)
# pylint: disable=protected-access
out = get_report()
self.assertTrue('1 hour(s)' in out)
self.assertTrue('1 hour(s)' in out or '2 hour(s)' in out)
self.assertTrue('VALID' in out and not 'INVALID' in out)
cert.target_expiry += datetime.timedelta(days=1)
+1
View File
@@ -76,6 +76,7 @@ class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods
return output.getvalue()
@test_util.broken_on_windows
@mock.patch("certbot.cli.flag_default")
def test_cli_ini_domains(self, mock_flag_default):
tmp_config = tempfile.NamedTemporaryFile()
+12 -7
View File
@@ -48,18 +48,23 @@ class NamespaceConfigTest(test_util.ConfigTestCase):
mock_constants.TEMP_CHECKPOINT_DIR = 't'
self.assertEqual(
self.config.accounts_dir, os.path.join(
self.config.config_dir, 'acc/acme-server.org:443/new'))
os.path.normpath(self.config.accounts_dir),
os.path.normpath(os.path.join(self.config.config_dir, 'acc/acme-server.org:443/new')))
self.assertEqual(
self.config.backup_dir, os.path.join(self.config.work_dir, 'backups'))
os.path.normpath(self.config.backup_dir),
os.path.normpath(os.path.join(self.config.work_dir, 'backups')))
self.assertEqual(
self.config.csr_dir, os.path.join(self.config.config_dir, 'csr'))
os.path.normpath(self.config.csr_dir),
os.path.normpath(os.path.join(self.config.config_dir, 'csr')))
self.assertEqual(
self.config.in_progress_dir, os.path.join(self.config.work_dir, '../p'))
os.path.normpath(self.config.in_progress_dir),
os.path.normpath(os.path.join(self.config.work_dir, '../p')))
self.assertEqual(
self.config.key_dir, os.path.join(self.config.config_dir, 'keys'))
os.path.normpath(self.config.key_dir),
os.path.normpath(os.path.join(self.config.config_dir, 'keys')))
self.assertEqual(
self.config.temp_checkpoint_dir, os.path.join(self.config.work_dir, 't'))
os.path.normpath(self.config.temp_checkpoint_dir),
os.path.normpath(os.path.join(self.config.work_dir, 't')))
def test_absolute_paths(self):
from certbot.configuration import NamespaceConfig
+1 -2
View File
@@ -140,7 +140,7 @@ class ImportCSRFileTest(unittest.TestCase):
util.CSR(file=csrfile,
data=data_pem,
form="pem"),
["Example.com"],),
["Example.com"]),
self._call(csrfile, data))
def test_pem_csr(self):
@@ -376,7 +376,6 @@ class NotAfterTest(unittest.TestCase):
class Sha256sumTest(unittest.TestCase):
"""Tests for certbot.crypto_util.notAfter"""
def test_sha256sum(self):
from certbot.crypto_util import sha256sum
self.assertEqual(sha256sum(CERT_PATH),
+9 -4
View File
@@ -1,6 +1,9 @@
"""Test certbot.display.completer."""
import os
import readline
try:
import readline # pylint: disable=import-error
except ImportError:
import certbot.display.dummy_readline as readline # type: ignore
import string
import sys
import unittest
@@ -9,9 +12,9 @@ import mock
from six.moves import reload_module # pylint: disable=import-error
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
from certbot.tests.util import TempDirTestCase
import certbot.tests.util as test_util
class CompleterTest(TempDirTestCase):
class CompleterTest(test_util.TempDirTestCase):
"""Test certbot.display.completer.Completer."""
def setUp(self):
@@ -47,6 +50,8 @@ class CompleterTest(TempDirTestCase):
completion = my_completer.complete(self.tempdir, num_paths)
self.assertEqual(completion, None)
@unittest.skipIf('readline' not in sys.modules,
reason='Not relevant if readline is not available.')
def test_import_error(self):
original_readline = sys.modules['readline']
sys.modules['readline'] = None
@@ -91,7 +96,7 @@ class CompleterTest(TempDirTestCase):
def enable_tab_completion(unused_command):
"""Enables readline tab completion using the system specific syntax."""
libedit = 'libedit' in readline.__doc__
libedit = readline.__doc__ is not None and 'libedit' in readline.__doc__
command = 'bind ^I rl_complete' if libedit else 'tab: complete'
readline.parse_and_bind(command)

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