Merge branch 'master' into save-more-hooks

This commit is contained in:
Brad Warren
2017-01-04 12:24:53 -08:00
59 changed files with 1887 additions and 1219 deletions
+8 -6
View File
@@ -17,12 +17,7 @@ env:
matrix:
include:
- python: "2.7"
env: TOXENV=cover BOULDER_INTEGRATION=1
sudo: required
after_failure:
- sudo cat /var/log/mysql/error.log
- ps aux | grep mysql
services: docker
env: TOXENV=cover
- python: "2.7"
env: TOXENV=lint
- python: "2.7"
@@ -46,6 +41,13 @@ matrix:
- sudo cat /var/log/mysql/error.log
- ps aux | grep mysql
services: docker
- python: "2.7"
env: TOXENV=py27_install BOULDER_INTEGRATION=1
sudo: required
after_failure:
- sudo cat /var/log/mysql/error.log
- ps aux | grep mysql
services: docker
- sudo: required
env: TOXENV=apache_compat
services: docker
+38 -13
View File
@@ -1,6 +1,7 @@
"""Apache Configuration based off of Augeas Configurator."""
# pylint: disable=too-many-lines
import filecmp
import fnmatch
import logging
import os
import re
@@ -362,18 +363,24 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return vhost
def included_in_wildcard(self, names, target_name):
"""Helper function to see if alias is covered by wildcard"""
target_name = target_name.split(".")[::-1]
wildcards = [domain.split(".")[1:] for domain in
names if domain.startswith("*")]
for wildcard in wildcards:
if len(wildcard) > len(target_name):
continue
for idx, segment in enumerate(wildcard[::-1]):
if segment != target_name[idx]:
break
else:
# https://docs.python.org/2/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops
"""Is target_name covered by a wildcard?
:param names: server aliases
:type names: `collections.Iterable` of `str`
:param str target_name: name to compare with wildcards
:returns: True if target_name is covered by a wildcard,
otherwise, False
:rtype: bool
"""
# use lowercase strings because fnmatch can be case sensitive
target_name = target_name.lower()
for name in names:
name = name.lower()
# fnmatch treats "[seq]" specially and [ or ] characters aren't
# valid in Apache but Apache doesn't error out if they are present
if "[" not in name and fnmatch.fnmatch(target_name, name):
return True
return False
@@ -463,7 +470,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
zope.component.getUtility(interfaces.IDisplay).notification(
"Apache mod_macro seems to be in use in file(s):\n{0}"
"\n\nUnfortunately mod_macro is not yet supported".format(
"\n ".join(vhost_macro)))
"\n ".join(vhost_macro)), force_interactive=True)
return all_names
@@ -1012,6 +1019,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.parser.find_dir("ServerAlias", target_name,
start=vh_path, exclude=False)):
return
if self._has_matching_wildcard(vh_path, target_name):
return
if not self.parser.find_dir("ServerName", None,
start=vh_path, exclude=False):
self.parser.add_dir(vh_path, "ServerName", target_name)
@@ -1019,6 +1028,22 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.parser.add_dir(vh_path, "ServerAlias", target_name)
self._add_servernames(vhost)
def _has_matching_wildcard(self, vh_path, target_name):
"""Is target_name already included in a wildcard in the vhost?
:param str vh_path: Augeas path to the vhost
:param str target_name: name to compare with wildcards
:returns: True if there is a wildcard covering target_name in
the vhost in vhost_path, otherwise, False
:rtype: bool
"""
matches = self.parser.find_dir(
"ServerAlias", start=vh_path, exclude=False)
aliases = (self.aug.get(match) for match in matches)
return self.included_in_wildcard(aliases, target_name)
def _add_name_vhost_if_necessary(self, vhost):
"""Add NameVirtualHost Directives if necessary for new vhost.
+4 -2
View File
@@ -85,7 +85,8 @@ def _vhost_menu(domain, vhosts):
"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),
choices, help_label="More Info", ok_label="Select")
choices, help_label="More Info",
ok_label="Select", force_interactive=True)
except errors.MissingCommandlineFlag:
msg = ("Encountered vhost ambiguity but unable to ask for user guidance in "
"non-interactive mode. Currently Certbot needs each vhost to be "
@@ -100,4 +101,5 @@ def _vhost_menu(domain, vhosts):
def _more_info_vhost(vhost):
zope.component.getUtility(interfaces.IDisplay).notification(
"Virtual Host Information:{0}{1}{0}{2}".format(
os.linesep, "-" * (display_util.WIDTH - 4), str(vhost)))
os.linesep, "-" * (display_util.WIDTH - 4), str(vhost)),
force_interactive=True)
@@ -220,10 +220,6 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertRaises(
errors.PluginError, self.config.choose_vhost, "none.com")
def test_choosevhost_select_vhost_with_wildcard(self):
chosen_vhost = self.config.choose_vhost("blue.purple.com", temp=True)
self.assertEqual(self.vh_truth[6], chosen_vhost)
def test_findbest_continues_on_short_domain(self):
# pylint: disable=protected-access
chosen_vhost = self.config._find_best_vhost("purple.com")
@@ -1255,8 +1251,6 @@ class AugeasVhostsTest(util.ApacheTest):
self.config = util.get_apache_configurator(
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/augeas_vhosts")
def tearDown(self):
shutil.rmtree(self.temp_dir)
@@ -1281,5 +1275,41 @@ class AugeasVhostsTest(util.ApacheTest):
vhs = self.config.get_virtual_hosts()
self.assertEqual([], vhs)
def test_choose_vhost_with_matching_wildcard(self):
names = (
"an.example.net", "another.example.net", "an.other.example.net")
for name in names:
self.assertFalse(name in self.config.choose_vhost(name).aliases)
def test_choose_vhost_without_matching_wildcard(self):
mock_path = "certbot_apache.display_ops.select_vhost"
with mock.patch(mock_path, lambda _, vhosts: vhosts[0]):
for name in ("a.example.net", "other.example.net"):
self.assertTrue(name in self.config.choose_vhost(name).aliases)
def test_choose_vhost_wildcard_not_found(self):
mock_path = "certbot_apache.display_ops.select_vhost"
names = (
"abc.example.net", "not.there.tld", "aa.wildcard.tld"
)
with mock.patch(mock_path) as mock_select:
mock_select.return_value = self.config.vhosts[0]
for name in names:
orig_cc = mock_select.call_count
self.config.choose_vhost(name)
self.assertEqual(mock_select.call_count - orig_cc, 1)
def test_choose_vhost_wildcard_found(self):
mock_path = "certbot_apache.display_ops.select_vhost"
names = (
"ab.example.net", "a.wildcard.tld", "yetanother.example.net"
)
with mock.patch(mock_path) as mock_select:
mock_select.return_value = self.config.vhosts[0]
for name in names:
self.config.choose_vhost(name)
self.assertEqual(mock_select.call_count, 0)
if __name__ == "__main__":
unittest.main() # pragma: no cover
@@ -17,7 +17,8 @@ class SelectVhostTest(unittest.TestCase):
"""Tests for certbot_apache.display_ops.select_vhost."""
def setUp(self):
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
False))
self.base_dir = "/example_path"
self.vhosts = util.get_vh_truth(
self.base_dir, "debian_apache_2_4/multiple_vhosts")
@@ -0,0 +1,11 @@
<VirtualHost *:80>
ServerName wildcard.tld
ServerAlias ?.wildcard.tld
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
@@ -0,0 +1,11 @@
<VirtualHost *:80>
ServerName example.net
ServerAlias ??.example.net *.other.example.net *another.example.net
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
+2 -1
View File
@@ -64,7 +64,8 @@ class ParserTest(ApacheTest): # pytlint: disable=too-few-public-methods
vhost_root="debian_apache_2_4/multiple_vhosts/apache2/sites-available"):
super(ParserTest, self).setUp(test_dir, config_root, vhost_root)
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
False))
from certbot_apache.parser import ApacheParser
self.aug = augeas.Augeas(
+11
View File
@@ -29,10 +29,14 @@ class Addr(common.Addr):
:param bool default: Whether the directive includes 'default_server'
"""
UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0')
CANONICAL_UNSPECIFIED_ADDRESS = UNSPECIFIED_IPV4_ADDRESSES[0]
def __init__(self, host, port, ssl, default):
super(Addr, self).__init__((host, port))
self.ssl = ssl
self.default = default
self.unspecified_address = host in self.UNSPECIFIED_IPV4_ADDRESSES
@classmethod
def fromstring(cls, str_addr):
@@ -96,6 +100,13 @@ class Addr(common.Addr):
def super_eq(self, other):
"""Check ip/port equality, with IPv6 support.
"""
# If both addresses got an unspecified address, then make sure the
# host representation in each match when doing the comparison.
if self.unspecified_address and other.unspecified_address:
return common.Addr((self.CANONICAL_UNSPECIFIED_ADDRESS,
self.tup[1]), self.ipv6) == \
common.Addr((other.CANONICAL_UNSPECIFIED_ADDRESS,
other.tup[1]), other.ipv6)
# Nginx plugin currently doesn't support IPv6 but this will
# future-proof it
return super(Addr, self).__eq__(other)
@@ -1,5 +1,6 @@
"""Test the helper objects in certbot_nginx.obj."""
import unittest
import itertools
class AddrTest(unittest.TestCase):
@@ -72,6 +73,24 @@ class AddrTest(unittest.TestCase):
self.assertNotEqual(self.addr1, self.addr2)
self.assertFalse(self.addr1 == 3333)
def test_equivalent_any_addresses(self):
from certbot_nginx.obj import Addr
any_addresses = ("0.0.0.0:80 default_server ssl",
"80 default_server ssl",
"*:80 default_server ssl")
for first, second in itertools.combinations(any_addresses, 2):
self.assertEqual(Addr.fromstring(first), Addr.fromstring(second))
# Also, make sure ports are checked.
self.assertNotEqual(Addr.fromstring(any_addresses[0]),
Addr.fromstring("0.0.0.0:443 default_server ssl"))
# And they aren't equivalent to a specified address.
for any_address in any_addresses:
self.assertNotEqual(
Addr.fromstring("192.168.1.2:80 default_server ssl"),
Addr.fromstring(any_address))
def test_set_inclusion(self):
from certbot_nginx.obj import Addr
set_a = set([self.addr1, self.addr2])
+139 -104
View File
@@ -6,10 +6,9 @@ import pytz
import traceback
import zope.component
from certbot import configuration
from certbot import errors
from certbot import interfaces
from certbot import renewal
from certbot import ocsp
from certbot import storage
from certbot import util
@@ -17,6 +16,10 @@ from certbot.display import util as display_util
logger = logging.getLogger(__name__)
###################
# Commands
###################
def update_live_symlinks(config):
"""Update the certificate file family symlinks to use archive_dir.
@@ -26,41 +29,28 @@ def update_live_symlinks(config):
.. note:: This assumes that the installation is using a Reverter object.
:param config: Configuration.
:type config: :class:`certbot.interfaces.IConfig`
:type config: :class:`certbot.configuration.NamespaceConfig`
"""
renewer_config = configuration.RenewerConfiguration(config)
for renewal_file in renewal.renewal_conf_files(renewer_config):
storage.RenewableCert(renewal_file,
configuration.RenewerConfiguration(renewer_config),
update_symlinks=True)
for renewal_file in storage.renewal_conf_files(config):
storage.RenewableCert(renewal_file, config, update_symlinks=True)
def rename_lineage(config):
"""Rename the specified lineage to the new name.
:param config: Configuration.
:type config: :class:`certbot.interfaces.IConfig`
:type config: :class:`certbot.configuration.NamespaceConfig`
"""
disp = zope.component.getUtility(interfaces.IDisplay)
renewer_config = configuration.RenewerConfiguration(config)
certname = config.certname
if not certname:
filenames = renewal.renewal_conf_files(renewer_config)
choices = [storage.lineagename_for_filename(name) for name in filenames]
if not choices:
raise errors.Error("No existing certificates found.")
code, index = disp.menu("Which certificate would you like to rename?",
choices, ok_label="Select", flag="--cert-name")
if code != display_util.OK or not index in range(0, len(choices)):
raise errors.Error("User ended interaction.")
certname = choices[index]
certname = _get_certname(config, "rename")
new_certname = config.new_certname
if not new_certname:
code, new_certname = disp.input("Enter the new name for certificate {0}"
.format(certname), flag="--updated-cert-name")
code, new_certname = disp.input(
"Enter the new name for certificate {0}".format(certname),
flag="--updated-cert-name", force_interactive=True)
if code != display_util.OK or not new_certname:
raise errors.Error("User ended interaction.")
@@ -68,77 +58,21 @@ def rename_lineage(config):
if not lineage:
raise errors.ConfigurationError("No existing certificate with name "
"{0} found.".format(certname))
storage.rename_renewal_config(certname, new_certname, renewer_config)
storage.rename_renewal_config(certname, new_certname, config)
disp.notification("Successfully renamed {0} to {1}."
.format(certname, new_certname), pause=False)
def _report_lines(msgs):
"""Format a results report for a category of single-line renewal outcomes"""
return " " + "\n ".join(str(msg) for msg in msgs)
def _report_human_readable(parsed_certs):
"""Format a results report for a parsed cert"""
certinfo = []
for cert in parsed_certs:
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
if cert.is_test_cert:
expiration_text = "INVALID: TEST CERT"
elif cert.target_expiry <= now:
expiration_text = "INVALID: EXPIRED"
else:
diff = cert.target_expiry - now
if diff.days == 1:
expiration_text = "VALID: 1 day"
elif diff.days < 1:
expiration_text = "VALID: {0} hour(s)".format(diff.seconds // 3600)
else:
expiration_text = "VALID: {0} days".format(diff.days)
valid_string = "{0} ({1})".format(cert.target_expiry, expiration_text)
certinfo.append(" Certificate Name: {0}\n"
" Domains: {1}\n"
" Expiry Date: {2}\n"
" Certificate Path: {3}\n"
" Private Key Path: {4}".format(
cert.lineagename,
" ".join(cert.names()),
valid_string,
cert.fullchain,
cert.privkey))
return "\n".join(certinfo)
def _describe_certs(parsed_certs, parse_failures):
"""Print information about the certs we know about"""
out = []
notify = out.append
if not parsed_certs and not parse_failures:
notify("No certs found.")
else:
if parsed_certs:
notify("Found the following certs:")
notify(_report_human_readable(parsed_certs))
if parse_failures:
notify("\nThe following renewal configuration files "
"were invalid:")
notify(_report_lines(parse_failures))
disp = zope.component.getUtility(interfaces.IDisplay)
disp.notification("\n".join(out), pause=False, wrap=False)
def certificates(config):
"""Display information about certs configured with Certbot
:param config: Configuration.
:type config: :class:`certbot.interfaces.IConfig`
:type config: :class:`certbot.configuration.NamespaceConfig`
"""
renewer_config = configuration.RenewerConfiguration(config)
parsed_certs = []
parse_failures = []
for renewal_file in renewal.renewal_conf_files(renewer_config):
for renewal_file in storage.renewal_conf_files(config):
try:
renewal_candidate = storage.RenewableCert(renewal_file,
configuration.RenewerConfiguration(config))
renewal_candidate = storage.RenewableCert(renewal_file, config)
parsed_certs.append(renewal_candidate)
except Exception as e: # pylint: disable=broad-except
logger.warning("Renewal configuration file %s produced an "
@@ -147,29 +81,19 @@ def certificates(config):
parse_failures.append(renewal_file)
# Describe all the certs
_describe_certs(parsed_certs, parse_failures)
_describe_certs(config, parsed_certs, parse_failures)
def _search_lineages(config, func, initial_rv):
"""Iterate func over unbroken lineages, allowing custom return conditions.
def delete(config):
"""Delete Certbot files associated with a certificate lineage."""
certname = _get_certname(config, "delete")
storage.delete_files(config, certname)
disp = zope.component.getUtility(interfaces.IDisplay)
disp.notification("Deleted all files relating to certificate {0}."
.format(certname), pause=False)
Allows flexible customization of return values, including multiple
return values and complex checks.
"""
cli_config = configuration.RenewerConfiguration(config)
configs_dir = cli_config.renewal_configs_dir
# Verify the directory is there
util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
rv = initial_rv
for renewal_file in renewal.renewal_conf_files(cli_config):
try:
candidate_lineage = storage.RenewableCert(renewal_file, cli_config)
except (errors.CertStorageError, IOError):
logger.debug("Renewal conf file %s is broken. Skipping.", renewal_file)
logger.debug("Traceback was:\n%s", traceback.format_exc())
continue
rv = func(candidate_lineage, rv)
return rv
###################
# Public Helpers
###################
def lineage_for_certname(config, certname):
"""Find a lineage object with name certname."""
@@ -215,3 +139,114 @@ def find_duplicative_certs(config, domains):
return (identical_names_cert, subset_names_cert)
return _search_lineages(config, update_certs_for_domain_matches, (None, None))
###################
# Private Helpers
###################
def _get_certname(config, verb):
"""Get certname from flag, interactively, or error out.
"""
certname = config.certname
if not certname:
disp = zope.component.getUtility(interfaces.IDisplay)
filenames = storage.renewal_conf_files(config)
choices = [storage.lineagename_for_filename(name) for name in filenames]
if not choices:
raise errors.Error("No existing certificates found.")
code, index = disp.menu("Which certificate would you like to {0}?".format(verb),
choices, ok_label="Select", flag="--cert-name")
if code != display_util.OK or not index in range(0, len(choices)):
raise errors.Error("User ended interaction.")
certname = choices[index]
return certname
def _report_lines(msgs):
"""Format a results report for a category of single-line renewal outcomes"""
return " " + "\n ".join(str(msg) for msg in msgs)
def _report_human_readable(config, parsed_certs):
"""Format a results report for a parsed cert"""
certinfo = []
checker = ocsp.RevocationChecker()
for cert in parsed_certs:
if config.certname and cert.lineagename != config.certname:
continue
if config.domains and not set(config.domains).issubset(cert.names()):
continue
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
reasons = []
if cert.is_test_cert:
reasons.append('TEST_CERT')
if cert.target_expiry <= now:
reasons.append('EXPIRED')
if checker.ocsp_revoked(cert.cert, cert.chain):
reasons.append('REVOKED')
if reasons:
status = "INVALID: " + ", ".join(reasons)
else:
diff = cert.target_expiry - now
if diff.days == 1:
status = "VALID: 1 day"
elif diff.days < 1:
status = "VALID: {0} hour(s)".format(diff.seconds // 3600)
else:
status = "VALID: {0} days".format(diff.days)
valid_string = "{0} ({1})".format(cert.target_expiry, status)
certinfo.append(" Certificate Name: {0}\n"
" Domains: {1}\n"
" Expiry Date: {2}\n"
" Certificate Path: {3}\n"
" Private Key Path: {4}".format(
cert.lineagename,
" ".join(cert.names()),
valid_string,
cert.fullchain,
cert.privkey))
return "\n".join(certinfo)
def _describe_certs(config, parsed_certs, parse_failures):
"""Print information about the certs we know about"""
out = []
notify = out.append
if not parsed_certs and not parse_failures:
notify("No certs found.")
else:
if parsed_certs:
match = "matching " if config.certname or config.domains else ""
notify("Found the following {0}certs:".format(match))
notify(_report_human_readable(config, parsed_certs))
if parse_failures:
notify("\nThe following renewal configuration files "
"were invalid:")
notify(_report_lines(parse_failures))
disp = zope.component.getUtility(interfaces.IDisplay)
disp.notification("\n".join(out), pause=False, wrap=False)
def _search_lineages(cli_config, func, initial_rv):
"""Iterate func over unbroken lineages, allowing custom return conditions.
Allows flexible customization of return values, including multiple
return values and complex checks.
"""
configs_dir = cli_config.renewal_configs_dir
# Verify the directory is there
util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
rv = initial_rv
for renewal_file in storage.renewal_conf_files(cli_config):
try:
candidate_lineage = storage.RenewableCert(renewal_file, cli_config)
except (errors.CertStorageError, IOError):
logger.debug("Renewal conf file %s is broken. Skipping.", renewal_file)
logger.debug("Traceback was:\n%s", traceback.format_exc())
continue
rv = func(candidate_lineage, rv)
return rv
+275 -134
View File
@@ -50,51 +50,56 @@ cli_command = LEAUTO if fragment in sys.argv[0] else "certbot"
# to replace as much of it as we can...
# This is the stub to include in help generated by argparse
SHORT_USAGE = """
{0} [SUBCOMMAND] [options] [-d domain] [-d domain] ...
{0} [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...
Certbot can obtain and install HTTPS/TLS/SSL certificates. By default,
it will attempt to use a webserver both for obtaining and installing the
cert. Major SUBCOMMANDS are:
cert. """.format(cli_command)
(default) run Obtain & install a cert in your current webserver
certonly Obtain cert, but do not install it (aka "auth")
install Install a previously obtained cert in a server
renew Renew previously obtained certs that are near expiry
revoke Revoke a previously obtained certificate
register Perform tasks related to registering with the CA
rollback Rollback server configuration changes made during install
config_changes Show changes made to server config during installation
update_symlinks Update cert symlinks based on renewal config file
rename Update a certificate's name
plugins Display information about installed plugins
certificates Display information about certs configured with Certbot
# This section is used for --help and --help all ; it needs information
# about installed plugins to be fully formatted
COMMAND_OVERVIEW = """The most common SUBCOMMANDS and flags are:
""".format(cli_command)
# This is the short help for certbot --help, where we disable argparse
# altogether
USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert:
obtain, install, and renew certificates:
(default) run Obtain & install a cert in your current webserver
certonly Obtain or renew a cert, but do not install it
renew Renew all previously obtained certs that are near expiry
-d DOMAINS Comma-separated list of domains to obtain a cert for
%s
--standalone Run a standalone webserver for authentication
%s
--webroot Place files in a server's webroot folder for authentication
--script User provided shell scripts for authentication
--manual Obtain certs interactively, or using shell script hooks
OR use different plugins to obtain (authenticate) the cert and then install it:
-n Run non-interactively
--test-cert Obtain a test cert from a staging server
--dry-run Test "renew" or "certonly" without saving any certs to disk
--authenticator standalone --installer apache
manage certificates:
certificates Display information about certs you have from Certbot
revoke Revoke a certificate (supply --cert-path)
rename Rename a certificate
delete Delete a certificate
manage your account with Let's Encrypt:
register Create a Let's Encrypt ACME account
--agree-tos Agree to the ACME server's Subscriber Agreement
-m EMAIL Email address for important account notifications
"""
# This is the short help for certbot --help, where we disable argparse
# altogether
HELP_USAGE = """
More detailed help:
-h, --help [topic] print this message, or detailed help on a topic;
the available topics are:
-h, --help [TOPIC] print this message, or detailed help on a topic;
the available TOPICS are:
all, automation, paths, security, testing, or any of the subcommands or
plugins (certonly, renew, install, register, nginx, apache, standalone,
webroot, script, etc.)
all, automation, commands, paths, security, testing, or any of the
subcommands or plugins (certonly, renew, install, register, nginx,
apache, standalone, webroot, etc.)
"""
@@ -140,19 +145,6 @@ def report_config_interaction(modified, modifiers):
VAR_MODIFIERS.setdefault(var, set()).update(modifiers)
def usage_strings(plugins):
"""Make usage strings late so that plugins can be initialised late"""
if "nginx" in plugins:
nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
else:
nginx_doc = "(nginx support is experimental, buggy, and not installed by default)"
if "apache" in plugins:
apache_doc = "--apache Use the Apache plugin for authentication & installation"
else:
apache_doc = "(the apache plugin is not installed)"
return USAGE % (apache_doc, nginx_doc), SHORT_USAGE
def possible_deprecation_warning(config):
"A deprecation warning for users with the old, not-self-upgrading letsencrypt-auto."
if cli_command != LEAUTO:
@@ -308,6 +300,104 @@ class HelpfulArgumentGroup(object):
"""Add a new command line argument to the argument group."""
self._parser.add(self._topic, *args, **kwargs)
class CustomHelpFormatter(argparse.HelpFormatter):
"""This is a clone of ArgumentDefaultsHelpFormatter, with bugfixes.
In particular we fix https://bugs.python.org/issue28742
"""
def _get_help_string(self, action):
helpstr = action.help
if '%(default)' not in action.help and '(default:' not in action.help:
if action.default != argparse.SUPPRESS:
defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
if action.option_strings or action.nargs in defaulting_nargs:
helpstr += ' (default: %(default)s)'
return helpstr
# The attributes here are:
# short: a string that will be displayed by "certbot -h commands"
# opts: a string that heads the section of flags with which this command is documented,
# both for "cerbot -h SUBCOMMAND" and "certbot -h all"
# usage: an optional string that overrides the header of "certbot -h SUBCOMMAND"
VERB_HELP = [
("run (default)", {
"short": "Obtain/renew a certificate, and install it",
"opts": "Options for obtaining & installing certs",
"usage": SHORT_USAGE.replace("[SUBCOMMAND]", ""),
"realname": "run"
}),
("certonly", {
"short": "Obtain or renew a certificate, but do not install it",
"opts": "Options for modifying how a cert is obtained",
"usage": ("\n\n certbot certonly [options] [-d DOMAIN] [-d DOMAIN] ...\n\n"
"This command obtains a TLS/SSL certificate without installing it anywhere.")
}),
("renew", {
"short": "Renew all certificates (or one specifed with --cert-name)",
"opts": ("The 'renew' subcommand will attempt to renew all"
" certificates (or more precisely, certificate lineages) you have"
" previously obtained if they are close to expiry, and print a"
" summary of the results. By default, 'renew' will reuse the options"
" used to create obtain or most recently successfully renew each"
" certificate lineage. You can try it with `--dry-run` first. For"
" more fine-grained control, you can renew individual lineages with"
" the `certonly` subcommand. Hooks are available to run commands"
" before and after renewal; see"
" https://certbot.eff.org/docs/using.html#renewal for more"
" information on these."),
"usage": "\n\n certbot renew [--cert-name NAME] [options]\n\n"
}),
("certificates", {
"short": "List certificates managed by Certbot",
"opts": "List certificates managed by Certbot",
"usage": ("\n\n certbot certificates [options] ...\n\n"
"Print information about the status of certificates managed by Certbot.")
}),
("delete", {
"short": "Clean up all files related to a certificate",
"opts": "Options for deleting a certificate"
}),
("revoke", {
"short": "Revoke a certificate specified with --cert-path",
"opts": "Options for revocation of certs",
"usage": "\n\n certbot revoke --cert-path /path/to/fullchain.pem [options]\n\n"
}),
("rename", {
"short": "Change a certificate's name (for management purposes)",
"opts": "Options for changing certificate names"
}),
("register", {
"short": "Register for account with Let's Encrypt / other ACME server",
"opts": "Options for account registration & modification"
}),
("install", {
"short": "Install an arbitrary cert in a server",
"opts": "Options for modifying how a cert is deployed"
}),
("config_changes", {
"short": "Show changes that Certbot has made to server configurations",
"opts": "Options for controlling which changes are displayed"
}),
("rollback", {
"short": "Roll back server conf changes made during cert installation",
"opts": "Options for rolling back server configuration changes"
}),
("plugins", {
"short": "List plugins that are installed and available on your system",
"opts": 'Options for for the "plugins" subcommand'
}),
("update_symlinks", {
"short": "Recreate symlinks in your /live/ directory",
"opts": ("Recreates cert and key symlinks in {0}, if you changed them by hand "
"or edited a renewal configuration file".format(
os.path.join(flag_default("config_dir"), "live")))
}),
]
# VERB_HELP is a list in order to preserve order, but a dict is sometimes useful
VERB_HELP_MAP = dict(VERB_HELP)
class HelpfulArgumentParser(object):
"""Argparse Wrapper.
@@ -318,6 +408,7 @@ class HelpfulArgumentParser(object):
"""
def __init__(self, args, plugins, detect_defaults=False):
from certbot import main
self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert,
@@ -326,26 +417,17 @@ class HelpfulArgumentParser(object):
"register": main.register, "renew": main.renew,
"revoke": main.revoke, "rollback": main.rollback,
"everything": main.run, "update_symlinks": main.update_symlinks,
"certificates": main.certificates, "rename": main.rename}
"certificates": main.certificates, "rename": main.rename,
"delete": main.delete}
# List of topics for which additional help can be provided
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS)
HELP_TOPICS += self.COMMANDS_TOPICS + ["manage"]
plugin_names = list(plugins)
self.help_topics = HELP_TOPICS + plugin_names + [None]
usage, short_usage = usage_strings(plugins)
self.parser = configargparse.ArgParser(
prog="certbot",
usage=short_usage,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
args_for_setting_config_path=["-c", "--config"],
default_config_files=flag_default("config_files"))
# This is the only way to turn off overly verbose config flag documentation
self.parser._add_config_file_help = False # pylint: disable=protected-access
self.detect_defaults = detect_defaults
self.args = args
self.determine_verb()
help1 = self.prescan_for_flag("-h", self.help_topics)
@@ -354,13 +436,72 @@ class HelpfulArgumentParser(object):
self.help_arg = help1 or help2
else:
self.help_arg = help1 if isinstance(help1, str) else help2
if self.help_arg is True:
# just --help with no topic; avoid argparse altogether
print(usage)
sys.exit(0)
short_usage = self._usage_string(plugins, self.help_arg)
self.visible_topics = self.determine_help_topics(self.help_arg)
self.groups = {} # elements are added by .add_group()
self.defaults = {} # elements are added by .parse_args()
self.defaults = {} # elements are added by .parse_args()
self.parser = configargparse.ArgParser(
prog="certbot",
usage=short_usage,
formatter_class=CustomHelpFormatter,
args_for_setting_config_path=["-c", "--config"],
default_config_files=flag_default("config_files"),
config_arg_help_message="path to config file (default: {0})".format(
" and ".join(flag_default("config_files"))))
# This is the only way to turn off overly verbose config flag documentation
self.parser._add_config_file_help = False # pylint: disable=protected-access
# Help that are synonyms for --help subcommands
COMMANDS_TOPICS = ["command", "commands", "subcommand", "subcommands", "verbs"]
def _list_subcommands(self):
longest = max(len(v) for v in VERB_HELP_MAP.keys())
text = "The full list of available SUBCOMMANDS is:\n\n"
for verb, props in sorted(VERB_HELP):
doc = props.get("short", "")
text += '{0:<{length}} {1}\n'.format(verb, doc, length=longest)
text += "\nYou can get more help on a specific subcommand with --help SUBCOMMAND\n"
return text
def _usage_string(self, plugins, help_arg):
"""Make usage strings late so that plugins can be initialised late
:param plugins: all discovered plugins
:param help_arg: False for none; True for --help; "TOPIC" for --help TOPIC
:rtype: str
:returns: a short usage string for the top of --help TOPIC)
"""
if "nginx" in plugins:
nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
else:
nginx_doc = "(the certbot nginx plugin is not installed)"
if "apache" in plugins:
apache_doc = "--apache Use the Apache plugin for authentication & installation"
else:
apache_doc = "(the cerbot apache plugin is not installed)"
usage = SHORT_USAGE
if help_arg == True:
print(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_USAGE)
sys.exit(0)
elif help_arg in self.COMMANDS_TOPICS:
print(usage + self._list_subcommands())
sys.exit(0)
elif help_arg == "all":
# if we're doing --help all, the OVERVIEW is part of the SHORT_USAGE at
# the top; if we're doing --help someothertopic, it's OT so it's not
usage += COMMAND_OVERVIEW % (apache_doc, nginx_doc)
else:
custom = VERB_HELP_MAP.get(help_arg, {}).get("usage", None)
usage = custom if custom else usage
return usage
def parse_args(self):
"""Parses command line arguments and returns the result.
@@ -382,8 +523,17 @@ class HelpfulArgumentParser(object):
# Do any post-parsing homework here
if self.verb == "renew":
if parsed_args.force_interactive:
raise errors.Error(
"{0} cannot be used with renew".format(
constants.FORCE_INTERACTIVE_FLAG))
parsed_args.noninteractive_mode = True
if parsed_args.force_interactive and parsed_args.noninteractive_mode:
raise errors.Error(
"Flag for non-interactive mode and {0} conflict".format(
constants.FORCE_INTERACTIVE_FLAG))
if parsed_args.staging or parsed_args.dry_run:
self.set_test_server(parsed_args)
@@ -562,7 +712,7 @@ class HelpfulArgumentParser(object):
util.add_deprecated_argument(
self.parser.add_argument, argument_name, num_args)
def add_group(self, topic, **kwargs):
def add_group(self, topic, verbs=(), **kwargs):
"""Create a new argument group.
This method must be called once for every topic, however, calls
@@ -570,6 +720,8 @@ class HelpfulArgumentParser(object):
clarity.
:param str topic: Name of the new argument group.
:param str verbs: List of subcommands that should be documented as part of
this help group / topic
:returns: The new argument group.
:rtype: `HelpfulArgumentGroup`
@@ -577,6 +729,9 @@ class HelpfulArgumentParser(object):
"""
if self.visible_topics[topic]:
self.groups[topic] = self.parser.add_argument_group(topic, **kwargs)
if self.help_arg:
for v in verbs:
self.groups[topic].add_argument(v, help=VERB_HELP_MAP[v]["short"])
return HelpfulArgumentGroup(self, topic)
@@ -617,32 +772,17 @@ class HelpfulArgumentParser(object):
def _add_all_groups(helpful):
helpful.add_group("automation", description="Arguments for automating execution & other tweaks")
helpful.add_group("security", description="Security parameters & server settings")
helpful.add_group(
"testing", description="The following flags are meant for "
"testing purposes only! Do NOT change them, unless you "
"really know what you're doing!")
# VERBS
helpful.add_group(
"renew", description="The 'renew' subcommand will attempt to renew all"
" certificates (or more precisely, certificate lineages) you have"
" previously obtained if they are close to expiry, and print a"
" summary of the results. By default, 'renew' will reuse the options"
" used to create obtain or most recently successfully renew each"
" certificate lineage. You can try it with `--dry-run` first. For"
" more fine-grained control, you can renew individual lineages with"
" the `certonly` subcommand. Hooks are available to run commands"
" before and after renewal; see"
" https://certbot.eff.org/docs/using.html#renewal for more"
" information on these.")
helpful.add_group("certonly", description="Options for modifying how a cert is obtained")
helpful.add_group("install", description="Options for modifying how a cert is deployed")
helpful.add_group("revoke", description="Options for revocation of certs")
helpful.add_group("rollback", description="Options for reverting config changes")
helpful.add_group("plugins", description='Options for the "plugins" subcommand')
helpful.add_group("config_changes",
description="Options for showing a history of config changes")
helpful.add_group("testing",
description="The following flags are meant for testing and integration purposes only.")
helpful.add_group("paths", description="Arguments changing execution paths & servers")
helpful.add_group("manage",
description="Various subcommands and flags are available for managing your certificates:",
verbs=["certificates", "delete", "renew", "revoke", "rename"])
# VERBS
for verb, docs in VERB_HELP:
name = docs.get("realname", verb)
helpful.add_group(name, description=docs["opts"])
def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: disable=too-many-statements
@@ -671,28 +811,33 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
None, "-t", "--text", dest="text_mode", action="store_true",
help=argparse.SUPPRESS)
helpful.add(
[None, "automation"], "-n", "--non-interactive", "--noninteractive",
[None, "automation", "run", "certonly"], "-n", "--non-interactive", "--noninteractive",
dest="noninteractive_mode", action="store_true",
help="Run without ever asking for user input. This may require "
"additional command line flags; the client will try to explain "
"which ones are required if it finds one missing")
helpful.add(
[None, "run", "certonly"],
[None, "register", "run", "certonly"],
constants.FORCE_INTERACTIVE_FLAG, action="store_true",
help="Force Certbot to be interactive even if it detects it's not "
"being run in a terminal. This flag cannot be used with the "
"renew subcommand.")
helpful.add(
[None, "run", "certonly", "certificates"],
"-d", "--domains", "--domain", dest="domains",
metavar="DOMAIN", action=_DomainsAction, default=[],
help="Domain names to apply. For multiple domains you can use "
"multiple -d flags or enter a comma separated list of domains "
"as a parameter.")
"as a parameter. (default: Ask)")
helpful.add(
[None, "run", "certonly"],
[None, "run", "certonly", "manage", "rename", "delete", "certificates"],
"--cert-name", dest="certname",
metavar="CERTNAME", default=None,
help="Certificate name to apply. Only one certificate name can be used "
"per Certbot run. To see certificate names, run 'certbot certificates'."
"If there is no existing certificate with this name and "
"domains are requested, create a new certificate with this name.")
"per Certbot run. To see certificate names, run 'certbot certificates'. "
"When creating a new certificate, specifies the new certificate's name.")
helpful.add(
"rename",
["rename", "manage"],
"--updated-cert-name", dest="new_certname",
metavar="NEW_CERTNAME", default=None,
help="New name for the certificate. Must be a valid filename.")
@@ -724,18 +869,18 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
help="With the register verb, indicates that details associated "
"with an existing registration, such as the e-mail address, "
"should be updated, rather than registering a new account.")
helpful.add(None, "-m", "--email", help=config_help("email"))
helpful.add(["register", "automation"], "-m", "--email", help=config_help("email"))
helpful.add(
["automation", "renew", "certonly", "run"],
["automation", "certonly", "run"],
"--keep-until-expiring", "--keep", "--reinstall",
dest="reinstall", action="store_true",
help="If the requested cert matches an existing cert, always keep the "
"existing one until it is due for renewal (for the "
"'run' subcommand this means reinstall the existing cert)")
"'run' subcommand this means reinstall the existing cert). (default: Ask)")
helpful.add(
"automation", "--expand", action="store_true",
help="If an existing cert covers some subset of the requested names, "
"always expand and replace it with the additional names.")
"always expand and replace it with the additional names. (default: Ask)")
helpful.add(
"automation", "--version", action="version",
version="%(prog)s {0}".format(certbot.__version__),
@@ -764,7 +909,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
"at this system. This option cannot be used with --csr.")
helpful.add(
"automation", "--agree-tos", dest="tos", action="store_true",
help="Agree to the ACME Subscriber Agreement")
help="Agree to the ACME Subscriber Agreement (default: Ask)")
helpful.add(
"automation", "--account", metavar="ACCOUNT_ID",
help="Account ID to use")
@@ -778,15 +923,17 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
helpful.add(
"automation", "--no-self-upgrade", action="store_true",
help="(certbot-auto only) prevent the certbot-auto script from"
" upgrading itself to newer released versions")
" upgrading itself to newer released versions (default: Upgrade"
" automatically)")
helpful.add(
["automation", "renew", "certonly"],
["automation", "renew", "certonly", "run"],
"-q", "--quiet", dest="quiet", action="store_true",
help="Silence all output except errors. Useful for automation via cron."
" Implies --non-interactive.")
# overwrites server, handled in HelpfulArgumentParser.parse_args()
helpful.add("testing", "--test-cert", "--staging", action='store_true', dest='staging',
help='Use the staging server to obtain test (invalid) certs; equivalent'
helpful.add(["testing", "revoke", "run"], "--test-cert", "--staging",
action='store_true', dest='staging',
help='Use the staging server to obtain or revoke test (invalid) certs; equivalent'
' to --server ' + constants.STAGING_URI)
helpful.add(
"testing", "--debug", action="store_true",
@@ -797,11 +944,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
help=config_help("no_verify_ssl"),
default=flag_default("no_verify_ssl"))
helpful.add(
["certonly", "renew", "run"], "--tls-sni-01-port", type=int,
["testing", "standalone", "apache", "nginx"], "--tls-sni-01-port", type=int,
default=flag_default("tls_sni_01_port"),
help=config_help("tls_sni_01_port"))
helpful.add(
["certonly", "renew", "run", "manual"], "--http-01-port", type=int,
["testing", "standalone", "manual"], "--http-01-port", type=int,
dest="http01_port",
default=flag_default("http01_port"), help=config_help("http01_port"))
helpful.add(
@@ -817,11 +964,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
helpful.add(
"security", "--redirect", action="store_true",
help="Automatically redirect all HTTP traffic to HTTPS for the newly "
"authenticated vhost.", dest="redirect", default=None)
"authenticated vhost. (default: Ask)", dest="redirect", default=None)
helpful.add(
"security", "--no-redirect", action="store_false",
help="Do not automatically redirect all HTTP traffic to HTTPS for the newly "
"authenticated vhost.", dest="redirect", default=None)
"authenticated vhost. (default: Ask)", dest="redirect", default=None)
helpful.add(
"security", "--hsts", action="store_true",
help="Add the Strict-Transport-Security header to every HTTP response."
@@ -829,8 +976,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
" Defends against SSL Stripping.", dest="hsts", default=False)
helpful.add(
"security", "--no-hsts", action="store_false",
help="Do not automatically add the Strict-Transport-Security header"
" to every HTTP response.", dest="hsts", default=False)
help=argparse.SUPPRESS, dest="hsts", default=False)
helpful.add(
"security", "--uir", action="store_true",
help="Add the \"Content-Security-Policy: upgrade-insecure-requests\""
@@ -838,9 +984,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
" https:// for every http:// resource.", dest="uir", default=None)
helpful.add(
"security", "--no-uir", action="store_false",
help="Do not automatically set the \"Content-Security-Policy:"
" upgrade-insecure-requests\" header to every HTTP response.",
dest="uir", default=None)
help=argparse.SUPPRESS, dest="uir", default=None)
helpful.add(
"security", "--staple-ocsp", action="store_true",
help="Enables OCSP Stapling. A valid OCSP response is stapled to"
@@ -848,14 +992,13 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
dest="staple", default=None)
helpful.add(
"security", "--no-staple-ocsp", action="store_false",
help="Do not automatically enable OCSP Stapling.",
dest="staple", default=None)
help=argparse.SUPPRESS, dest="staple", default=None)
helpful.add(
"security", "--strict-permissions", action="store_true",
help="Require that all configuration files are owned by the current "
"user; only needed if your config is somewhere unsafe like /tmp/")
helpful.add(
["manual", "standalone", "certonly", "renew", "run"],
["manual", "standalone", "certonly", "renew"],
"--preferred-challenges", dest="pref_challs",
action=_PrefChallAction, default=[],
help='A sorted, comma delimited list of the preferred challenge to '
@@ -897,7 +1040,8 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
" see if the programs being run are in the $PATH, so that mistakes can"
" be caught early, even when the hooks aren't being run just yet. The"
" validation is rather simplistic and fails if you use more advanced"
" shell constructs, so you can use this switch to disable it.")
" shell constructs, so you can use this switch to disable it."
" (default: False)")
helpful.add_deprecated_argument("--agree-dev-preview", 0)
helpful.add_deprecated_argument("--dialog", 0)
@@ -917,12 +1061,15 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
def _create_subparsers(helpful):
helpful.add("config_changes", "--num", type=int,
help="How many past revisions you want to be displayed")
from certbot.client import sample_user_agent # avoid import loops
helpful.add(
None, "--user-agent", default=None,
help="Set a custom user agent string for the client. User agent strings allow "
"the CA to collect high level statistics about success rates by OS and "
"plugin. If you wish to hide your server OS version from the Let's "
'Encrypt server, set this to "".')
'Encrypt server, set this to "". '
'(default: {0})'.format(sample_user_agent()))
helpful.add("certonly",
"--csr", type=read_file,
help="Path to a Certificate Signing Request (CSR) in DER or PEM format."
@@ -949,10 +1096,8 @@ def _paths_parser(helpful):
if verb == "help":
verb = helpful.help_arg
cph = "Path to where cert is saved (with auth --csr), installed from or revoked."
section = "paths"
if verb in ("install", "revoke", "certonly"):
section = verb
cph = "Path to where cert is saved (with auth --csr), installed from, or revoked."
section = ["paths", "install", "revoke", "certonly", "manage"]
if verb == "certonly":
add(section, "--cert-path", type=os.path.abspath,
default=flag_default("auth_cert_path"), help=cph)
@@ -974,7 +1119,7 @@ def _paths_parser(helpful):
default_cp = None
if verb == "certonly":
default_cp = flag_default("auth_chain_path")
add("paths", "--fullchain-path", default=default_cp, type=os.path.abspath,
add(["install", "paths"], "--fullchain-path", default=default_cp, type=os.path.abspath,
help="Accompanying path to a full certificate chain (cert plus chain).")
add("paths", "--chain-path", default=default_cp, type=os.path.abspath,
help="Accompanying path to a certificate chain.")
@@ -997,24 +1142,20 @@ def _plugins_parsing(helpful, plugins):
"a particular plugin by setting options provided below. Running "
"--help <plugin_name> will list flags specific to that plugin.")
helpful.add(
"plugins", "-a", "--authenticator", help="Authenticator plugin name.")
helpful.add(
"plugins", "-i", "--installer", help="Installer plugin name (also used to find domains).")
helpful.add(
"plugins", "--configurator", help="Name of the plugin that is "
"both an authenticator and an installer. Should not be used "
"together with --authenticator or --installer.")
helpful.add(["plugins", "certonly", "run", "install"],
helpful.add("plugins", "--configurator",
help="Name of the plugin that is both an authenticator and an installer."
" Should not be used together with --authenticator or --installer. "
"(default: Ask)")
helpful.add("plugins", "-a", "--authenticator", help="Authenticator plugin name.")
helpful.add("plugins", "-i", "--installer",
help="Installer plugin name (also used to find domains).")
helpful.add(["plugins", "certonly", "run", "install", "config_changes"],
"--apache", action="store_true",
help="Obtain and install certs using Apache")
helpful.add(["plugins", "certonly", "run", "install"],
"--nginx", action="store_true",
help="Obtain and install certs using Nginx")
helpful.add(["plugins", "certonly", "run", "install", "config_changes"],
"--nginx", action="store_true", help="Obtain and install certs using Nginx")
helpful.add(["plugins", "certonly"], "--standalone", action="store_true",
help='Obtain certs using a "standalone" webserver.')
helpful.add(["plugins", "certonly"], "--script", action="store_true",
help='Obtain certs using shell script(s)')
helpful.add(["plugins", "certonly"], "--manual", action="store_true",
help='Provide laborious manual instructions for obtaining a cert')
helpful.add(["plugins", "certonly"], "--webroot", action="store_true",
+13 -4
View File
@@ -15,7 +15,6 @@ import certbot
from certbot import account
from certbot import auth_handler
from certbot import configuration
from certbot import constants
from certbot import crypto_util
from certbot import errors
@@ -38,11 +37,11 @@ def acme_from_config_key(config, key):
"Wrangle ACME client construction"
# TODO: Allow for other alg types besides RS256
net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl),
user_agent=_determine_user_agent(config))
user_agent=determine_user_agent(config))
return acme_client.Client(config.server, key=key, net=net)
def _determine_user_agent(config):
def determine_user_agent(config):
"""
Set a user_agent string in the config based on the choice of plugins.
(this wasn't knowable at construction time)
@@ -59,6 +58,16 @@ def _determine_user_agent(config):
ua = config.user_agent
return ua
def sample_user_agent():
"Document what this Certbot's user agent string will be like."
class DummyConfig(object):
"Shim for computing a sample user agent."
def __init__(self):
self.authenticator = "XXX"
self.installer = "YYY"
self.user_agent = None
return determine_user_agent(DummyConfig())
def register(config, account_storage, tos_cb=None):
"""Register new account with an ACME CA.
@@ -297,7 +306,7 @@ class Client(object):
new_name, OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped),
key.pem, crypto_util.dump_pyopenssl_chain(chain),
configuration.RenewerConfiguration(self.config.namespace))
self.config)
def save_certificate(self, certr, chain_cert,
cert_path, chain_path, fullchain_path):
+8 -16
View File
@@ -25,9 +25,16 @@ class NamespaceConfig(object):
- `csr_dir`
- `in_progress_dir`
- `key_dir`
- `renewer_config_file`
- `temp_checkpoint_dir`
And the following paths are dynamically resolved using
:attr:`~certbot.interfaces.IConfig.config_dir` and relative
paths defined in :py:mod:`certbot.constants`:
- `default_archive_dir`
- `live_dir`
- `renewal_configs_dir`
:ivar namespace: Namespace typically produced by
:meth:`argparse.ArgumentParser.parse_args`.
:type namespace: :class:`argparse.Namespace`
@@ -85,16 +92,6 @@ class NamespaceConfig(object):
new_ns = copy.deepcopy(self.namespace)
return type(self)(new_ns)
class RenewerConfiguration(object):
"""Configuration wrapper for renewer."""
def __init__(self, namespace):
self.namespace = namespace
def __getattr__(self, name):
return getattr(self.namespace, name)
@property
def default_archive_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR)
@@ -108,11 +105,6 @@ class RenewerConfiguration(object):
return os.path.join(
self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR)
@property
def renewer_config_file(self): # pylint: disable=missing-docstring
return os.path.join(
self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME)
def check_config_sanity(config):
"""Validate command line options and display error message if
+2 -2
View File
@@ -93,5 +93,5 @@ TEMP_CHECKPOINT_DIR = "temp_checkpoint"
RENEWAL_CONFIGS_DIR = "renewal"
"""Renewal configs directory, relative to `IConfig.config_dir`."""
RENEWER_CONFIG_FILENAME = "renewer.conf"
"""Renewer config file name (relative to `IConfig.config_dir`)."""
FORCE_INTERACTIVE_FLAG = "--force-interactive"
"""Flag to disable TTY checking in IDisplay."""
+2 -1
View File
@@ -48,7 +48,8 @@ def redirect_by_default():
code, selection = util(interfaces.IDisplay).menu(
"Please choose whether HTTPS access is required or optional.",
choices, default=0, cli_flag="--redirect / --no-redirect")
choices, default=0,
cli_flag="--redirect / --no-redirect", force_interactive=True)
if code != display_util.OK:
return False
+7 -5
View File
@@ -46,7 +46,8 @@ def get_email(invalid=False, optional=True):
while True:
try:
code, email = z_util(interfaces.IDisplay).input(
invalid_prefix + msg if invalid else msg)
invalid_prefix + msg if invalid else msg,
force_interactive=True)
except errors.MissingCommandlineFlag:
msg = ("You should register before running non-interactively, "
"or provide --agree-tos and --email <email_address> flags.")
@@ -79,7 +80,7 @@ def choose_account(accounts):
labels = [acc.slug for acc in accounts]
code, index = z_util(interfaces.IDisplay).menu(
"Please choose an account", labels)
"Please choose an account", labels, force_interactive=True)
if code == display_util.OK:
return accounts[index]
else:
@@ -157,7 +158,7 @@ def _filter_names(names):
code, names = z_util(interfaces.IDisplay).checklist(
"Which names would you like to activate HTTPS for?",
tags=sorted_names, cli_flag="--domains")
tags=sorted_names, cli_flag="--domains", force_interactive=True)
return code, [str(s) for s in names]
@@ -173,7 +174,7 @@ def _choose_names_manually(prompt_prefix=""):
code, input_ = z_util(interfaces.IDisplay).input(
prompt_prefix +
"Please enter in your domain name(s) (comma and/or space separated) ",
cli_flag="--domains")
cli_flag="--domains", force_interactive=True)
if code == display_util.OK:
invalid_domains = dict()
@@ -211,7 +212,8 @@ def _choose_names_manually(prompt_prefix=""):
if retry_message:
# We had error in input
retry = z_util(interfaces.IDisplay).yesno(retry_message)
retry = z_util(interfaces.IDisplay).yesno(retry_message,
force_interactive=True)
if retry:
return _choose_names_manually()
else:
+122 -23
View File
@@ -1,14 +1,18 @@
"""Certbot display."""
import logging
import os
import textwrap
import sys
import six
import zope.interface
from certbot import constants
from certbot import interfaces
from certbot import errors
from certbot.display import completer
logger = logging.getLogger(__name__)
WIDTH = 72
@@ -50,19 +54,25 @@ def _wrap_lines(msg):
@zope.interface.implementer(interfaces.IDisplay)
class FileDisplay(object):
"""File-based display."""
# pylint: disable=too-many-arguments
# see https://github.com/certbot/certbot/issues/3915
def __init__(self, outfile):
def __init__(self, outfile, force_interactive):
super(FileDisplay, self).__init__()
self.outfile = outfile
self.force_interactive = force_interactive
self.skipped_interaction = False
def notification(self, message, pause=True, wrap=True):
# pylint: disable=unused-argument
def notification(self, message, pause=True,
wrap=True, force_interactive=False):
"""Displays a notification and waits for user acceptance.
:param str message: Message to display
:param bool pause: Whether or not the program should pause for the
user's confirmation
:param bool wrap: Whether or not the application should wrap text
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
"""
side_frame = "-" * 79
@@ -72,10 +82,14 @@ class FileDisplay(object):
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
line=os.linesep, frame=side_frame, msg=message))
if pause:
six.moves.input("Press Enter to Continue")
if self._can_interact(force_interactive):
six.moves.input("Press Enter to Continue")
else:
logger.debug("Not pausing for user confirmation")
def menu(self, message, choices, ok_label="", cancel_label="",
help_label="", **unused_kwargs):
help_label="", default=None,
cli_flag=None, force_interactive=False, **unused_kwargs):
# pylint: disable=unused-argument
"""Display a menu.
@@ -86,7 +100,10 @@ class FileDisplay(object):
:param choices: Menu lines, len must be > 0
:type choices: list of tuples (tag, item) or
list of descriptions (tags will be enumerated)
:param dict _kwargs: absorbs default / cli_args
:param default: default value to return (if one exists)
:param str cli_flag: option used to set this value with the CLI
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
:returns: tuple of (`code`, `index`) where
`code` - str display exit code
@@ -95,18 +112,25 @@ class FileDisplay(object):
:rtype: tuple
"""
if self._return_default(message, default, cli_flag, force_interactive):
return OK, default
self._print_menu(message, choices)
code, selection = self._get_valid_int_ans(len(choices))
return code, selection - 1
def input(self, message, **unused_kwargs):
def input(self, message, default=None,
cli_flag=None, force_interactive=False, **unused_kwargs):
# pylint: disable=no-self-use
"""Accept input from the user.
:param str message: message to display to the user
:param dict _kwargs: absorbs default / cli_args
:param default: default value to return (if one exists)
:param str cli_flag: option used to set this value with the CLI
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
:returns: tuple of (`code`, `input`) where
`code` - str display exit code
@@ -114,6 +138,9 @@ class FileDisplay(object):
:rtype: tuple
"""
if self._return_default(message, default, cli_flag, force_interactive):
return OK, default
ans = six.moves.input(
textwrap.fill(
"%s (Enter 'c' to cancel): " % message,
@@ -126,7 +153,8 @@ class FileDisplay(object):
else:
return OK, ans
def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs):
def yesno(self, message, yes_label="Yes", no_label="No", default=None,
cli_flag=None, force_interactive=False, **unused_kwargs):
"""Query the user with a yes/no question.
Yes and No label must begin with different letters, and must contain at
@@ -135,12 +163,18 @@ class FileDisplay(object):
:param str message: question for the user
:param str yes_label: Label of the "Yes" parameter
:param str no_label: Label of the "No" parameter
:param dict _kwargs: absorbs default / cli_args
:param default: default value to return (if one exists)
:param str cli_flag: option used to set this value with the CLI
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
:returns: True for "Yes", False for "No"
:rtype: bool
"""
if self._return_default(message, default, cli_flag, force_interactive):
return default
side_frame = ("-" * 79) + os.linesep
message = _wrap_lines(message)
@@ -162,14 +196,18 @@ class FileDisplay(object):
ans.startswith(no_label[0].upper())):
return False
def checklist(self, message, tags, default_status=True, **unused_kwargs):
def checklist(self, message, tags, default_status=True, default=None,
cli_flag=None, force_interactive=False, **unused_kwargs):
# pylint: disable=unused-argument
"""Display a checklist.
:param str message: Message to display to user
:param list tags: `str` tags to select, len(tags) > 0
:param bool default_status: Not used for FileDisplay
:param dict _kwargs: absorbs default / cli_args
:param default: default value to return (if one exists)
:param str cli_flag: option used to set this value with the CLI
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
:returns: tuple of (`code`, `tags`) where
`code` - str display exit code
@@ -177,12 +215,16 @@ class FileDisplay(object):
:rtype: tuple
"""
if self._return_default(message, default, cli_flag, force_interactive):
return OK, default
while True:
self._print_menu(message, tags)
code, ans = self.input("Select the appropriate numbers separated "
"by commas and/or spaces, or leave input "
"blank to select all options shown")
"blank to select all options shown",
force_interactive=True)
if code == OK:
if len(ans.strip()) == 0:
@@ -197,10 +239,65 @@ class FileDisplay(object):
else:
return code, []
def directory_select(self, message, **unused_kwargs):
def _return_default(self, prompt, default, cli_flag, force_interactive):
"""Should we return the default instead of prompting the user?
:param str prompt: prompt for the user
:param default: default answer to prompt
:param str cli_flag: command line option for setting an answer
to this question
:param bool force_interactive: if interactivity is forced by the
IDisplay call
:returns: True if we should return the default without prompting
:rtype: bool
"""
msg = "Invalid IDisplay call for this prompt:\n{0}".format(prompt)
if cli_flag:
msg += ("\nYou can set an answer to "
"this prompt with the {0} flag".format(cli_flag))
assert default is not None or force_interactive, msg
if self._can_interact(force_interactive):
return False
else:
logger.debug(
"Falling back to default %s for the prompt:\n%s",
default, prompt)
return True
def _can_interact(self, force_interactive):
"""Can we safely interact with the user?
:param bool force_interactive: if interactivity is forced by the
IDisplay call
:returns: True if the display can interact with the user
:rtype: bool
"""
if (self.force_interactive or force_interactive or
sys.stdin.isatty() and self.outfile.isatty()):
return True
elif not self.skipped_interaction:
logger.warning(
"Skipped user interaction because Certbot doesn't appear to "
"be running in a terminal. You should probably include "
"--non-interactive or %s on the command line.",
constants.FORCE_INTERACTIVE_FLAG)
self.skipped_interaction = True
return False
def directory_select(self, message, default=None, cli_flag=None,
force_interactive=False, **unused_kwargs):
"""Display a directory selection screen.
:param str message: prompt to give the user
:param default: default value to return (if one exists)
:param str cli_flag: option used to set this value with the CLI
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
:returns: tuple of the form (`code`, `string`) where
`code` - display exit code
@@ -208,7 +305,7 @@ class FileDisplay(object):
"""
with completer.Completer():
return self.input(message)
return self.input(message, default, cli_flag, force_interactive)
def _scrub_checklist_input(self, indices, tags):
# pylint: disable=no-self-use
@@ -310,7 +407,7 @@ class FileDisplay(object):
class NoninteractiveDisplay(object):
"""An iDisplay implementation that never asks for interactive user input"""
def __init__(self, outfile):
def __init__(self, outfile, *unused_args, **unused_kwargs):
super(NoninteractiveDisplay, self).__init__()
self.outfile = outfile
@@ -324,7 +421,7 @@ class NoninteractiveDisplay(object):
msg += "\n\n(You can set this with the {0} flag)".format(cli_flag)
raise errors.MissingCommandlineFlag(msg)
def notification(self, message, pause=False, wrap=True):
def notification(self, message, pause=False, wrap=True, **unused_kwargs):
# pylint: disable=unused-argument
"""Displays a notification without waiting for user acceptance.
@@ -341,7 +438,7 @@ class NoninteractiveDisplay(object):
line=os.linesep, frame=side_frame, msg=message))
def menu(self, message, choices, ok_label=None, cancel_label=None,
help_label=None, default=None, cli_flag=None):
help_label=None, default=None, cli_flag=None, **unused_kwargs):
# pylint: disable=unused-argument,too-many-arguments
"""Avoid displaying a menu.
@@ -364,7 +461,7 @@ class NoninteractiveDisplay(object):
return OK, default
def input(self, message, default=None, cli_flag=None):
def input(self, message, default=None, cli_flag=None, **unused_kwargs):
"""Accept input from the user.
:param str message: message to display to the user
@@ -381,7 +478,8 @@ class NoninteractiveDisplay(object):
else:
return OK, default
def yesno(self, message, yes_label=None, no_label=None, default=None, cli_flag=None):
def yesno(self, message, yes_label=None, no_label=None,
default=None, cli_flag=None, **unused_kwargs):
# pylint: disable=unused-argument
"""Decide Yes or No, without asking anybody
@@ -398,8 +496,8 @@ class NoninteractiveDisplay(object):
else:
return default
def checklist(self, message, tags, default=None, cli_flag=None, **kwargs):
# pylint: disable=unused-argument
def checklist(self, message, tags, default=None,
cli_flag=None, **unused_kwargs):
"""Display a checklist.
:param str message: Message to display to user
@@ -417,7 +515,8 @@ class NoninteractiveDisplay(object):
else:
return OK, default
def directory_select(self, message, default=None, cli_flag=None):
def directory_select(self, message, default=None,
cli_flag=None, **unused_kwargs):
"""Simulate prompting the user for a directory.
This function returns default if it is not ``None``, otherwise,
+4 -1
View File
@@ -108,7 +108,10 @@ def execute(shell_cmd):
:returns: `tuple` (`str` stderr, `str` stdout)"""
cmd = Popen(shell_cmd, shell=True, stdout=PIPE, stderr=PIPE)
# universal_newlines causes Popen.communicate()
# to return str objects instead of bytes in Python 3
cmd = Popen(shell_cmd, shell=True, stdout=PIPE,
stderr=PIPE, universal_newlines=True)
out, err = cmd.communicate()
if cmd.returncode != 0:
logger.error('Hook command "%s" returned error code %d',
+49 -19
View File
@@ -138,15 +138,15 @@ class IAuthenticator(IPlugin):
"""
def get_chall_pref(domain):
"""Return list of challenge preferences.
"""Return `collections.Iterable` of challenge preferences.
:param str domain: Domain for which challenge preferences are sought.
:returns: List of challenge types (subclasses of
:returns: `collections.Iterable` of challenge types (subclasses of
:class:`acme.challenges.Challenge`) with the most
preferred challenges first. If a type is not specified, it means the
Authenticator cannot perform the challenge.
:rtype: list
:rtype: `collections.Iterable`
"""
@@ -158,7 +158,7 @@ class IAuthenticator(IPlugin):
instances, such that it contains types found within
:func:`get_chall_pref` only.
:returns: List of ACME
:returns: `collections.Iterable` of ACME
:class:`~acme.challenges.ChallengeResponse` instances
or if the :class:`~acme.challenges.Challenge` cannot
be fulfilled then:
@@ -168,7 +168,7 @@ class IAuthenticator(IPlugin):
``False``
Authenticator will never be able to perform (error).
:rtype: :class:`list` of
:rtype: :class:`collections.Iterable` of
:class:`acme.challenges.ChallengeResponse`,
where responses are required to be returned in
the same order as corresponding input challenges
@@ -201,7 +201,7 @@ class IConfig(zope.interface.Interface):
"""
server = zope.interface.Attribute("ACME Directory Resource URI.")
email = zope.interface.Attribute(
"Email used for registration and recovery contact.")
"Email used for registration and recovery contact. (default: Ask)")
rsa_key_size = zope.interface.Attribute("Size of the RSA key.")
must_staple = zope.interface.Attribute(
"Adds the OCSP Must Staple extension to the certificate. "
@@ -223,9 +223,6 @@ class IConfig(zope.interface.Interface):
temp_checkpoint_dir = zope.interface.Attribute(
"Temporary checkpoint directory.")
renewer_config_file = zope.interface.Attribute(
"Location of renewal configuration file.")
no_verify_ssl = zope.interface.Attribute(
"Disable verification of the ACME server's certificate.")
tls_sni_01_port = zope.interface.Attribute(
@@ -257,7 +254,7 @@ class IInstaller(IPlugin):
def get_all_names():
"""Returns all names that may be authenticated.
:rtype: `list` of `str`
:rtype: `collections.Iterable` of `str`
"""
@@ -292,11 +289,11 @@ class IInstaller(IPlugin):
"""
def supported_enhancements():
"""Returns a list of supported enhancements.
"""Returns a `collections.Iterable` of supported enhancements.
:returns: supported enhancements which should be a subset of
:const:`~certbot.constants.ENHANCEMENTS`
:rtype: :class:`list` of :class:`str`
:rtype: :class:`collections.Iterable` of :class:`str`
"""
@@ -364,21 +361,29 @@ class IInstaller(IPlugin):
class IDisplay(zope.interface.Interface):
"""Generic display."""
# pylint: disable=too-many-arguments
# see https://github.com/certbot/certbot/issues/3915
def notification(message, pause, wrap=True):
def notification(message, pause, wrap=True, force_interactive=False):
"""Displays a string message
:param str message: Message to display
:param bool pause: Whether or not the application should pause for
confirmation (if available)
:param bool wrap: Whether or not the application should wrap text
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
"""
def menu(message, choices, ok_label="OK", # pylint: disable=too-many-arguments
cancel_label="Cancel", help_label="", default=None, cli_flag=None):
def menu(message, choices, ok_label="OK",
cancel_label="Cancel", help_label="",
default=None, cli_flag=None, force_interactive=False):
"""Displays a generic menu.
When not setting force_interactive=True, you must provide a
default value.
:param str message: message to display
:param choices: choices
@@ -389,6 +394,8 @@ class IDisplay(zope.interface.Interface):
:param str help_label: label for Help button
:param int default: default (non-interactive) choice from the menu
:param str cli_flag: to automate choice from the menu, eg "--keep"
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
:returns: tuple of (`code`, `index`) where
`code` - str display exit code
@@ -399,10 +406,16 @@ class IDisplay(zope.interface.Interface):
"""
def input(message, default=None, cli_args=None):
def input(message, default=None, cli_args=None, force_interactive=False):
"""Accept input from the user.
When not setting force_interactive=True, you must provide a
default value.
:param str message: message to display to the user
:param str default: default (non-interactive) response to prompt
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
:returns: tuple of (`code`, `input`) where
`code` - str display exit code
@@ -415,14 +428,19 @@ class IDisplay(zope.interface.Interface):
"""
def yesno(message, yes_label="Yes", no_label="No", default=None,
cli_args=None):
cli_args=None, force_interactive=False):
"""Query the user with a yes/no question.
Yes and No label must begin with different letters.
When not setting force_interactive=True, you must provide a
default value.
:param str message: question for the user
:param str default: default (non-interactive) choice from the menu
:param str cli_flag: to automate choice from the menu, eg "--redirect / --no-redirect"
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
:returns: True for "Yes", False for "No"
:rtype: bool
@@ -432,14 +450,20 @@ class IDisplay(zope.interface.Interface):
"""
def checklist(message, tags, default_state, default=None, cli_args=None):
def checklist(message, tags, default_state,
default=None, cli_args=None, force_interactive=False):
"""Allow for multiple selections from a menu.
When not setting force_interactive=True, you must provide a
default value.
:param str message: message to display to the user
:param list tags: where each is of type :class:`str` len(tags) > 0
:param bool default_status: If True, items are in a selected state by default.
:param str default: default (non-interactive) state of the checklist
:param str cli_flag: to automate choice from the menu, eg "--domains"
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
:returns: tuple of the form (code, list_tags) where
`code` - int display exit code
@@ -451,15 +475,21 @@ class IDisplay(zope.interface.Interface):
"""
def directory_select(self, message, default=None, cli_flag=None):
def directory_select(self, message, default=None,
cli_flag=None, force_interactive=False):
"""Display a directory selection screen.
When not setting force_interactive=True, you must provide a
default value.
:param str message: prompt to give the user
:param default: the default value to return, if one exists, when
using the NoninteractiveDisplay
:param str cli_flag: option used to set this value with the CLI,
if one exists, to be included in error messages given by
NoninteractiveDisplay
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
:returns: tuple of the form (`code`, `string`) where
`code` - int display exit code
+29 -7
View File
@@ -139,7 +139,8 @@ def _handle_subset_cert_request(config, domains, cert):
br=os.linesep)
if config.expand or config.renew_by_default or zope.component.getUtility(
interfaces.IDisplay).yesno(question, "Expand", "Cancel",
cli_flag="--expand"):
cli_flag="--expand",
force_interactive=True):
return "renew", cert
else:
reporter_util = zope.component.getUtility(interfaces.IReporter)
@@ -188,7 +189,8 @@ def _handle_identical_cert_request(config, lineage):
"Renew & replace the cert (limit ~5 per 7 days)"]
display = zope.component.getUtility(interfaces.IDisplay)
response = display.menu(question, choices, "OK", "Cancel", default=0)
response = display.menu(question, choices, "OK", "Cancel",
default=0, force_interactive=True)
if response[0] == display_util.CANCEL:
# TODO: Add notification related to command-line options for
# skipping the menu for this case.
@@ -282,17 +284,26 @@ def _find_domains_or_certname(config, installer):
"""Retrieve domains and certname from config or user input.
"""
domains = None
certname = config.certname
# first, try to get domains from the config
if config.domains:
domains = config.domains
elif not config.certname:
# if we can't do that but we have a certname, get the domains
# with that certname
elif certname:
domains = cert_manager.domains_for_certname(config, certname)
# that certname might not have existed, or there was a problem.
# try to get domains from the user.
if not domains:
domains = display_ops.choose_names(installer)
if not domains and not config.certname:
if not domains and not certname:
raise errors.Error("Please specify --domains, or --installer that "
"will help in domain names autodiscovery, or "
"--cert-name for an existing certificate name.")
return domains, config.certname
return domains, certname
def _report_new_cert(config, cert_path, fullchain_path):
@@ -365,7 +376,8 @@ def _determine_account(config):
"server at {1}".format(
regr.terms_of_service, config.server))
obj = zope.component.getUtility(interfaces.IDisplay)
return obj.yesno(msg, "Agree", "Cancel", cli_flag="--agree-tos")
return obj.yesno(msg, "Agree", "Cancel",
cli_flag="--agree-tos", force_interactive=True)
try:
acc, acme = client.register(
@@ -511,6 +523,14 @@ def rename(config, unused_plugins):
"""
cert_manager.rename_lineage(config)
def delete(config, unused_plugins):
"""Delete a certificate
Use the information in the config file to delete an existing
lineage.
"""
cert_manager.delete(config)
def certificates(config, unused_plugins):
"""Display information about certs configured with Certbot
"""
@@ -712,6 +732,7 @@ def _handle_exception(exc_type, exc_value, trace, config):
with open(logfile, "w") as logfd:
traceback.print_exception(
exc_type, exc_value, trace, file=logfd)
assert "--debug" not in sys.argv # config is None if this explodes
except: # pylint: disable=bare-except
sys.exit(tb_str)
if "--debug" in sys.argv:
@@ -779,7 +800,8 @@ def set_displayer(config):
elif config.noninteractive_mode:
displayer = display_util.NoninteractiveDisplay(sys.stdout)
else:
displayer = display_util.FileDisplay(sys.stdout)
displayer = display_util.FileDisplay(sys.stdout,
config.force_interactive)
zope.component.provideUtility(displayer)
def _post_logging_setup(config, plugins, cli_args):
+109
View File
@@ -0,0 +1,109 @@
"""Tools for checking certificate revocation."""
import logging
from subprocess import Popen, PIPE
from certbot import errors
from certbot import util
logger = logging.getLogger(__name__)
class RevocationChecker(object):
"This class figures out OCSP checking on this system, and performs it."
def __init__(self):
self.broken = False
if not util.exe_exists("openssl"):
logging.info("openssl not installed, can't check revocation")
self.broken = True
return
# New versions of openssl want -header var=val, old ones want -header var val
test_host_format = Popen(["openssl", "ocsp", "-header", "var", "val"],
stdout=PIPE, stderr=PIPE, universal_newlines=True)
_out, err = test_host_format.communicate()
if "Missing =" in err:
self.host_args = lambda host: ["Host=" + host]
else:
self.host_args = lambda host: ["Host", host]
def ocsp_revoked(self, cert_path, chain_path):
"""Get revoked status for a particular cert version.
.. todo:: Make this a non-blocking call
:param str cert_path: Path to certificate
:param str chain_path: Path to intermediate cert
:rtype bool or None:
:returns: True if revoked; False if valid or the check failed
"""
if self.broken:
return False
logger.debug("Querying OCSP for %s", cert_path)
url, host = self.determine_ocsp_server(cert_path)
if not host:
return False
# jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this!
cmd = ["openssl", "ocsp",
"-no_nonce",
"-issuer", chain_path,
"-cert", cert_path,
"-url", url,
"-CAfile", chain_path,
"-verify_other", chain_path,
"-header"] + self.host_args(host)
try:
output, err = util.run_script(cmd, log=logging.debug)
except errors.SubprocessError as e:
logger.info("OCSP check failed for %s (are we offline?)", cert_path)
logger.debug("Command was:\n%s\nError was:\n%s", " ".join(cmd), e)
return False
return _translate_ocsp_query(cert_path, output, err)
def determine_ocsp_server(self, cert_path):
"""Extract the OCSP server host from a certificate.
:param str cert_path: Path to the cert we're checking OCSP for
:rtype tuple:
:returns: (OCSP server URL or None, OCSP server host or None)
"""
try:
url, _err = util.run_script(
["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"],
log=logging.debug)
except errors.SubprocessError as e:
logger.info("Cannot extract OCSP URI from %s", cert_path)
logger.debug("Error was:\n%s", e)
return None, None
url = url.rstrip()
host = url.partition("://")[2].rstrip("/")
if host:
return url, host
else:
logger.info("Cannot process OCSP host from URL (%s) in cert at %s", url, cert_path)
return None, None
def _translate_ocsp_query(cert_path, ocsp_output, ocsp_errors):
"""Parse openssl's weird output to work out what it means."""
if not "Response verify OK" in ocsp_errors:
logger.info("Revocation status for %s is unknown", cert_path)
logger.debug("Uncertain ouput:\n%s\nstderr:\n%s", ocsp_output, ocsp_errors)
return False
if cert_path + ": good" in ocsp_output:
return False
elif cert_path + ": revoked" in ocsp_output:
return True
else:
logger.warn("Unable to properly parse OCSP output: %s", ocsp_output)
return False
+104 -200
View File
@@ -1,56 +1,49 @@
"""Manual plugin."""
"""Manual authenticator plugin"""
import os
import logging
import pipes
import shutil
import socket
import subprocess
import sys
import tempfile
import time
import six
import zope.component
import zope.interface
from acme import challenges
from acme import errors as acme_errors
from certbot import errors
from certbot import interfaces
from certbot import errors
from certbot import hooks
from certbot.plugins import common
logger = logging.getLogger(__name__)
@zope.interface.implementer(interfaces.IAuthenticator)
@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(common.Plugin):
"""Manual Authenticator.
"""Manual authenticator
This plugin requires user's manual intervention in setting up a HTTP
server for solving http-01 challenges and thus does not need to be
run as a privileged process. Alternatively shows instructions on how
to use Python's built-in HTTP server.
.. todo:: Support for `~.challenges.TLSSNI01`.
This plugin allows the user to perform the domain validation
challenge(s) themselves. This either be done manually by the user or
through shell scripts provided to Certbot.
"""
description = 'Manual configuration or run your own shell scripts'
hidden = True
description = "Manually configure an HTTP server"
MESSAGE_TEMPLATE = {
"dns-01": """\
long_description = (
'Authenticate through manual configuration or custom shell scripts. '
'When using shell scripts, an authenticator script must be provided. '
'The environment variables available to this script are '
'$CERTBOT_DOMAIN which contains the domain being authenticated, '
'$CERTBOT_VALIDATION which is the validation string, and '
'$CERTBOT_TOKEN which is the filename of the resource requested when '
'performing an HTTP-01 challenge. An additional cleanup script can '
'also be provided and can use the additional variable '
'$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth '
'script.')
_DNS_INSTRUCTIONS = """\
Please deploy a DNS TXT record under the name
{domain} with the following value:
{validation}
Once this is deployed,
""",
"http-01": """\
Once this is deployed,"""
_HTTP_INSTRUCTIONS = """\
Make sure your web server displays the following content at
{uri} before continuing:
@@ -59,203 +52,114 @@ Make sure your web server displays the following content at
If you don't have HTTP server configured, you can run the following
command on the target server (as root):
{command}
"""}
# a disclaimer about your current IP being transmitted to Let's Encrypt's servers.
IP_DISCLAIMER = """\
NOTE: The IP of this machine will be publicly logged as having requested this certificate. \
If you're running certbot in manual mode on a machine that is not your server, \
please ensure you're okay with that.
Are you OK with your IP being logged?
"""
# "cd /tmp/certbot" makes sure user doesn't serve /root,
# separate "public_html" ensures that cert.pem/key.pem are not
# served and makes it more obvious that Python command will serve
# anything recursively under the cwd
CMD_TEMPLATE = """\
mkdir -p {root}/public_html/{achall.URI_ROOT_PATH}
cd {root}/public_html
mkdir -p /tmp/certbot/public_html/{achall.URI_ROOT_PATH}
cd /tmp/certbot/public_html
printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token}
# run only once per server:
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
"import BaseHTTPServer, SimpleHTTPServer; \\
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
s.serve_forever()" """
"""Command template."""
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
self._root = (tempfile.mkdtemp() if self.conf("test-mode")
else "/tmp/certbot")
self._httpd = None
self.env = dict()
@classmethod
def add_parser_arguments(cls, add):
add("test-mode", action="store_true",
help="Test mode. Executes the manual command in subprocess.")
add("public-ip-logging-ok", action="store_true",
help="Automatically allows public IP logging.")
add('auth-hook',
help='Path or command to execute for the authentication script')
add('cleanup-hook',
help='Path or command to execute for the cleanup script')
add('public-ip-logging-ok', action='store_true',
help='Automatically allows public IP logging (default: Ask)')
def prepare(self): # pylint: disable=missing-docstring,no-self-use
if self.config.noninteractive_mode and not self.conf("test-mode"):
raise errors.PluginError("Running manual mode non-interactively is not supported")
def prepare(self): # pylint: disable=missing-docstring
if self.config.noninteractive_mode and not self.conf('auth-hook'):
raise errors.PluginError(
'An authentication script must be provided with --{0} when '
'using the manual plugin non-interactively.'.format(
self.option_name('auth-hook')))
self._validate_hooks()
def _validate_hooks(self):
if self.config.validate_hooks:
for name in ('auth-hook', 'cleanup-hook'):
hook = self.conf(name)
if hook is not None:
hook_prefix = self.option_name(name)[:-len('-hook')]
hooks.validate_hook(hook, hook_prefix)
def more_info(self): # pylint: disable=missing-docstring,no-self-use
return ("This plugin requires user's manual intervention in setting "
"up challenges to prove control of a domain and does not need "
"to be run as a privileged process. When solving "
"http-01 challenges, the user is responsible for setting up "
"an HTTP server. Alternatively, instructions are shown on how "
"to use Python's built-in HTTP server. The user is "
"responsible for configuration of a domain's DNS when solving "
"dns-01 challenges. The type of challenges used can be "
"controlled through the --preferred-challenges flag.")
return (
'This plugin allows the user to customize setup for domain '
'validation challenges either through shell scripts provided by '
'the user or by performing the setup manually.')
def get_chall_pref(self, domain):
# pylint: disable=missing-docstring,no-self-use,unused-argument
return [challenges.HTTP01, challenges.DNS01]
def perform(self, achalls):
# pylint: disable=missing-docstring
self._get_ip_logging_permission()
mapping = {"http-01": self._perform_http01_challenge,
"dns-01": self._perform_dns01_challenge}
def perform(self, achalls): # pylint: disable=missing-docstring
self._verify_ip_logging_ok()
if self.conf('auth-hook'):
perform_achall = self._perform_achall_with_script
else:
perform_achall = self._perform_achall_manually
responses = []
# TODO: group achalls by the same socket.gethostbyname(_ex)
# and prompt only once per server (one "echo -n" per domain)
for achall in achalls:
responses.append(mapping[achall.typ](achall))
perform_achall(achall)
responses.append(achall.response(achall.account_key))
return responses
@classmethod
def _test_mode_busy_wait(cls, port):
while True:
time.sleep(1)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect(("localhost", port))
except socket.error: # pragma: no cover
pass
def _verify_ip_logging_ok(self):
if not self.conf('public-ip-logging-ok'):
cli_flag = '--{0}'.format(self.option_name('public-ip-logging-ok'))
msg = ('NOTE: The IP of this machine will be publicly logged as '
"having requested this certificate. If you're running "
'certbot in manual mode on a machine that is not your '
"server, please ensure you're okay with that.\n\n"
'Are you OK with your IP being logged?')
display = zope.component.getUtility(interfaces.IDisplay)
if display.yesno(msg, cli_flag=cli_flag, force_interactive=True):
setattr(self.config, self.dest('public-ip-logging-ok'), True)
else:
break
finally:
sock.close()
raise errors.PluginError('Must agree to IP logging to proceed')
def cleanup(self, achalls):
# pylint: disable=missing-docstring
for achall in achalls:
if isinstance(achall.chall, challenges.HTTP01):
self._cleanup_http01_challenge(achall)
def _perform_http01_challenge(self, achall):
# same path for each challenge response would be easier for
# users, but will not work if multiple domains point at the
# same server: default command doesn't support virtual hosts
response, validation = achall.response_and_validation()
port = (response.port if self.config.http01_port is None
else int(self.config.http01_port))
command = self.CMD_TEMPLATE.format(
root=self._root, achall=achall, response=response,
# TODO(kuba): pipes still necessary?
validation=pipes.quote(validation),
encoded_token=achall.chall.encode("token"),
port=port)
if self.conf("test-mode"):
logger.debug("Test mode. Executing the manual command: %s", command)
# sh shipped with OS X does't support echo -n, but supports printf
try:
self._httpd = subprocess.Popen(
command,
# don't care about setting stdout and stderr,
# we're in test mode anyway
shell=True,
executable=None,
# "preexec_fn" is UNIX specific, but so is "command"
preexec_fn=os.setsid)
except OSError as error: # ValueError should not happen!
logger.debug(
"Couldn't execute manual command: %s", error, exc_info=True)
return False
logger.debug("Manual command running as PID %s.", self._httpd.pid)
# give it some time to bootstrap, before we try to verify
# (cert generation in case of simpleHttpS might take time)
self._test_mode_busy_wait(port)
if self._httpd.poll() is not None:
raise errors.Error("Couldn't execute manual command")
def _perform_achall_with_script(self, achall):
env = dict(CERTBOT_DOMAIN=achall.domain,
CERTBOT_VALIDATION=achall.validation(achall.account_key))
if isinstance(achall.chall, challenges.HTTP01):
env['CERTBOT_TOKEN'] = achall.chall.encode('token')
else:
self._notify_and_wait(
self._get_message(achall).format(
validation=validation,
response=response,
uri=achall.chall.uri(achall.domain),
command=command))
os.environ.pop('CERTBOT_TOKEN', None)
os.environ.update(env)
_, out = hooks.execute(self.conf('auth-hook'))
env['CERTBOT_AUTH_OUTPUT'] = out.strip()
self.env[achall.domain] = env
if not response.simple_verify(
achall.chall, achall.domain,
achall.account_key.public_key(), self.config.http01_port):
logger.warning("Self-verify of challenge failed.")
return response
def _perform_dns01_challenge(self, achall):
response, validation = achall.response_and_validation()
if not self.conf("test-mode"):
self._notify_and_wait(
self._get_message(achall).format(
validation=validation,
domain=achall.validation_domain_name(achall.domain),
response=response))
try:
verification_status = response.simple_verify(
achall.chall, achall.domain,
achall.account_key.public_key())
except acme_errors.DependencyError:
logger.warning("Self verification requires optional "
"dependency `dnspython` to be installed.")
def _perform_achall_manually(self, achall):
validation = achall.validation(achall.account_key)
if isinstance(achall.chall, challenges.HTTP01):
msg = self._HTTP_INSTRUCTIONS.format(
achall=achall, encoded_token=achall.chall.encode('token'),
port=self.config.http01_port,
uri=achall.chall.uri(achall.domain), validation=validation)
else:
if not verification_status:
logger.warning("Self-verify of challenge failed.")
assert isinstance(achall.chall, challenges.DNS01)
msg = self._DNS_INSTRUCTIONS.format(
domain=achall.validation_domain_name(achall.domain),
validation=validation)
display = zope.component.getUtility(interfaces.IDisplay)
display.notification(msg, wrap=False, force_interactive=True)
return response
def _cleanup_http01_challenge(self, achall):
# pylint: disable=missing-docstring,unused-argument
if self.conf("test-mode"):
assert self._httpd is not None, (
"cleanup() must be called after perform()")
if self._httpd.poll() is None:
logger.debug("Terminating manual command process")
self._httpd.terminate()
else:
logger.debug("Manual command process already terminated "
"with %s code", self._httpd.returncode)
shutil.rmtree(self._root)
def _notify_and_wait(self, message):
# pylint: disable=no-self-use
# TODO: IDisplay wraps messages, breaking the command
#answer = zope.component.getUtility(interfaces.IDisplay).notification(
# message=message, pause=True)
sys.stdout.write(message)
six.moves.input("Press ENTER to continue")
def _get_ip_logging_permission(self):
# pylint: disable=missing-docstring
if not (self.conf("test-mode") or self.conf("public-ip-logging-ok")):
if not zope.component.getUtility(interfaces.IDisplay).yesno(
self.IP_DISCLAIMER, "Yes", "No",
cli_flag="--manual-public-ip-logging-ok"):
raise errors.PluginError("Must agree to IP logging to proceed")
else:
self.config.namespace.manual_public_ip_logging_ok = True
def _get_message(self, achall):
# pylint: disable=missing-docstring,no-self-use,unused-argument
return self.MESSAGE_TEMPLATE.get(achall.chall.typ, "")
def cleanup(self, achalls): # pylint: disable=missing-docstring
if self.conf('cleanup-hook'):
for achall in achalls:
env = self.env.pop(achall.domain)
if 'CERTBOT_TOKEN' not in env:
os.environ.pop('CERTBOT_TOKEN', None)
os.environ.update(env)
hooks.execute(self.conf('cleanup-hook'))
+79 -101
View File
@@ -1,134 +1,112 @@
"""Tests for certbot.plugins.manual."""
"""Tests for certbot.plugins.manual"""
import os
import unittest
import six
import mock
from acme import challenges
from acme import errors as acme_errors
from acme import jose
from certbot import achallenges
from certbot import errors
from certbot.tests import acme_util
from certbot.tests import util as test_util
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
class AuthenticatorTest(unittest.TestCase):
"""Tests for certbot.plugins.manual.Authenticator."""
def setUp(self):
from certbot.plugins.manual import Authenticator
self.http_achall = acme_util.HTTP01_A
self.dns_achall = acme_util.DNS01_A
self.achalls = [self.http_achall, self.dns_achall]
self.config = mock.MagicMock(
http01_port=8080, manual_test_mode=False,
manual_public_ip_logging_ok=False, noninteractive_mode=True)
self.auth = Authenticator(config=self.config, name="manual")
http01_port=0, manual_auth_hook=None, manual_cleanup_hook=None,
manual_public_ip_logging_ok=False, noninteractive_mode=False,
validate_hooks=False)
self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)
self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY)
from certbot.plugins.manual import Authenticator
self.auth = Authenticator(self.config, name='manual')
self.achalls = [self.http01, self.dns01]
config_test_mode = mock.MagicMock(
http01_port=8080, manual_test_mode=True, noninteractive_mode=True)
self.auth_test_mode = Authenticator(
config=config_test_mode, name="manual")
def test_prepare(self):
def test_prepare_no_hook_noninteractive(self):
self.config.noninteractive_mode = True
self.assertRaises(errors.PluginError, self.auth.prepare)
self.auth_test_mode.prepare() # error not raised
def test_prepare_bad_hook(self):
self.config.manual_auth_hook = os.path.abspath(os.sep) # is / on UNIX
self.config.validate_hooks = True
self.assertRaises(errors.HookCommandNotFound, self.auth.prepare)
def test_more_info(self):
self.assertTrue(isinstance(self.auth.more_info(), str))
self.assertTrue(isinstance(self.auth.more_info(), six.string_types))
def test_get_chall_pref(self):
self.assertTrue(all(issubclass(pref, challenges.Challenge)
for pref in self.auth.get_chall_pref("foo.com")))
self.assertEqual(self.auth.get_chall_pref('example.org'),
[challenges.HTTP01, challenges.DNS01])
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
def test_perform_empty(self, mock_interaction):
mock_interaction().yesno.return_value = True
self.assertEqual([], self.auth.perform([]))
@mock.patch('certbot.plugins.manual.zope.component.getUtility')
def test_ip_logging_not_ok(self, mock_get_utility):
mock_get_utility().yesno.return_value = False
self.assertRaises(errors.PluginError, self.auth.perform, [])
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
@mock.patch("certbot.plugins.manual.sys.stdout")
@mock.patch("acme.challenges.HTTP01Response.simple_verify")
@mock.patch("six.moves.input")
def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction):
mock_verify.return_value = True
mock_interaction().yesno.return_value = True
@mock.patch('certbot.plugins.manual.zope.component.getUtility')
def test_ip_logging_ok(self, mock_get_utility):
mock_get_utility().yesno.return_value = True
self.auth.perform([])
self.assertTrue(self.config.manual_public_ip_logging_ok)
resp_http = self.http01.response(KEY)
resp_dns = self.dns01.response(KEY)
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_VALIDATION;')
dns_expected = '{0}\n{1}\n{2}'.format(
self.dns_achall.domain, 'notoken',
self.dns_achall.validation(self.dns_achall.account_key))
http_expected = '{0}\n{1}\n{2}'.format(
self.http_achall.domain, self.http_achall.chall.encode('token'),
self.http_achall.validation(self.http_achall.account_key))
self.assertEqual([resp_http, resp_dns], self.auth.perform(self.achalls))
self.assertEqual(2, mock_raw_input.call_count)
mock_verify.assert_called_with(
self.http01.challb.chall, "foo.com", KEY.public_key(), 8080)
self.assertEqual(
self.auth.perform(self.achalls),
[achall.response(achall.account_key) for achall in self.achalls])
self.assertEqual(
self.auth.env[self.dns_achall.domain]['CERTBOT_AUTH_OUTPUT'],
dns_expected)
self.assertEqual(
self.auth.env[self.http_achall.domain]['CERTBOT_AUTH_OUTPUT'],
http_expected)
message = mock_stdout.write.mock_calls[0][1][0]
self.assertTrue(self.http01.chall.encode("token") in message)
@mock.patch('certbot.plugins.manual.zope.component.getUtility')
def test_manual_perform(self, mock_get_utility):
self.config.manual_public_ip_logging_ok = True
self.assertEqual(
self.auth.perform(self.achalls),
[achall.response(achall.account_key) for achall in self.achalls])
for i, (args, kwargs) in enumerate(mock_get_utility().notification.call_args_list):
achall = self.achalls[i]
self.assertTrue(achall.validation(achall.account_key) in args[0])
self.assertFalse(kwargs['wrap'])
mock_verify.return_value = False
with mock.patch("certbot.plugins.manual.logger") as mock_logger:
self.auth.perform(self.achalls)
self.assertEqual(2, mock_logger.warning.call_count)
def test_cleanup(self):
self.config.manual_public_ip_logging_ok = True
self.config.manual_auth_hook = 'echo foo;'
self.config.manual_cleanup_hook = '# cleanup'
self.auth.perform(self.achalls)
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
@mock.patch("acme.challenges.DNS01Response.simple_verify")
@mock.patch("six.moves.input")
def test_perform_missing_dependency(self, mock_raw_input, mock_verify, mock_interaction):
mock_interaction().yesno.return_value = True
mock_verify.side_effect = acme_errors.DependencyError()
for achall in self.achalls:
self.auth.cleanup([achall])
self.assertEqual(os.environ['CERTBOT_AUTH_OUTPUT'], 'foo')
self.assertEqual(os.environ['CERTBOT_DOMAIN'], achall.domain)
self.assertEqual(
os.environ['CERTBOT_VALIDATION'],
achall.validation(achall.account_key))
with mock.patch("certbot.plugins.manual.logger") as mock_logger:
self.auth.perform([self.dns01])
self.assertEqual(1, mock_logger.warning.call_count)
mock_raw_input.assert_called_once_with("Press ENTER to continue")
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
@mock.patch("certbot.plugins.manual.Authenticator._notify_and_wait")
def test_disagree_with_ip_logging(self, mock_notify, mock_interaction):
mock_interaction().yesno.return_value = False
mock_notify.side_effect = errors.Error("Exception not raised, \
continued execution even after disagreeing with IP logging")
self.assertRaises(errors.PluginError, self.auth.perform, self.achalls)
@mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True)
def test_perform_test_command_oserror(self, mock_popen):
mock_popen.side_effect = OSError
self.assertEqual([False], self.auth_test_mode.perform([self.http01]))
@mock.patch("certbot.plugins.manual.socket.socket")
@mock.patch("certbot.plugins.manual.time.sleep", autospec=True)
@mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True)
def test_perform_test_command_run_failure(
self, mock_popen, unused_mock_sleep, unused_mock_socket):
mock_popen.poll.return_value = 10
mock_popen.return_value.pid = 1234
self.assertRaises(
errors.Error, self.auth_test_mode.perform, self.achalls)
def test_cleanup_test_mode_already_terminated(self):
# pylint: disable=protected-access
self.auth_test_mode._httpd = httpd = mock.Mock()
httpd.poll.return_value = 0
self.auth_test_mode.cleanup(self.achalls)
def test_cleanup_test_mode_kills_still_running(self):
# pylint: disable=protected-access
self.auth_test_mode._httpd = httpd = mock.Mock(pid=1234)
httpd.poll.return_value = None
self.auth_test_mode.cleanup(self.achalls)
httpd.terminate.assert_called_once_with()
if isinstance(achall.chall, challenges.HTTP01):
self.assertEqual(
os.environ['CERTBOT_TOKEN'],
achall.chall.encode('token'))
else:
self.assertFalse('CERTBOT_TOKEN' in os.environ)
if __name__ == "__main__":
if __name__ == '__main__':
unittest.main() # pragma: no cover
-161
View File
@@ -1,161 +0,0 @@
"""Script-based Authenticator."""
import logging
import os
import sys
import zope.interface
from acme import challenges
from certbot import errors
from certbot import interfaces
from certbot import hooks
from certbot.plugins import common
logger = logging.getLogger(__name__)
CHALLENGES = ["http-01", "dns-01"]
@zope.interface.implementer(interfaces.IAuthenticator)
@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(common.Plugin):
"""Script authenticator
calls user defined script to perform authentication and
optionally cleanup.
"""
description = "Authenticate using user provided script(s)"
long_description = ("Authenticate using user provided script(s). " +
"Authenticator script has the following environment " +
"variables available for it: " +
"CERTBOT_DOMAIN - The domain being authenticated " +
"CERTBOT_VALIDATION - The validation string " +
"CERTBOT_TOKEN - Resource name part of HTTP-01 " +
"challenge (HTTP-01 only). " +
"Cleanup script has all the above, and additional " +
"var: CERTBOT_AUTH_OUTPUT - stdout output from the " +
"authenticator"
)
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
self.cleanup_script = None
self.auth_script = None
self.challenges = []
@classmethod
def add_parser_arguments(cls, add):
add("auth", default=None, required=False,
help="path or command for the authentication script")
add("cleanup", default=None, required=False,
help="path or command for the cleanup script")
@property
def supported_challenges(self):
"""Challenges supported by this plugin."""
return self.challenges
def more_info(self): # pylint: disable=missing-docstring
return("This authenticator enables user to perform authentication " +
"using shell script(s).")
def prepare(self):
"""Prepare script plugin, check challenge, scripts and register them"""
pref_challenges = self.config.pref_challs
for c in pref_challenges:
if c.typ in CHALLENGES:
self.challenges.append(c)
if not self.challenges and len(pref_challenges):
# Challenges requested, but not supported
raise errors.PluginError(
"Unfortunately script plugin doesn't yet support " +
"the requested challenges")
# Challenge not defined on cli, set default
if not self.challenges:
self.challenges.append(challenges.Challenge.TYPES["http-01"])
if not self.conf("auth"):
raise errors.PluginError("Parameter --script-auth is required " +
"for script plugin")
self._prepare_scripts()
def _prepare_scripts(self):
"""Helper method for prepare, to take care of validating scripts"""
script_path = self.conf("auth")
cleanup_path = self.conf("cleanup")
if self.config.validate_hooks:
hooks.validate_hook(script_path, "script_auth")
self.auth_script = script_path
if cleanup_path:
if self.config.validate_hooks:
hooks.validate_hook(cleanup_path, "script_cleanup")
self.cleanup_script = cleanup_path
def get_chall_pref(self, domain):
"""Return challenge(s) we're answering to """
# pylint: disable=unused-argument
return self.challenges
def perform(self, achalls):
"""Perform the authentication per challenge"""
mapping = {"http-01": self._setup_env_http,
"dns-01": self._setup_env_dns}
responses = []
for achall in achalls:
response, validation = achall.response_and_validation()
# Setup env vars
mapping[achall.typ](achall, validation)
output = self.execute(self.auth_script)
if output:
self._write_auth_output(output)
responses.append(response)
return responses
def _setup_env_http(self, achall, validation):
"""Write environment variables for http challenge"""
ev = dict()
ev["CERTBOT_TOKEN"] = achall.chall.encode("token")
ev["CERTBOT_VALIDATION"] = validation
ev["CERTBOT_DOMAIN"] = achall.domain
os.environ.update(ev)
def _setup_env_dns(self, achall, validation):
"""Write environment variables for dns challenge"""
ev = dict()
ev["CERTBOT_VALIDATION"] = validation
ev["CERTBOT_DOMAIN"] = achall.domain
os.environ.update(ev)
def _write_auth_output(self, out):
"""Write output from auth script to env var for
cleanup to act upon"""
os.environ.update({"CERTBOT_AUTH_OUTPUT": out.strip()})
def _normalize_string(self, value):
"""Return string instead of bytestring for Python3.
Helper function for writing env vars, as os.environ needs str"""
if isinstance(value, bytes):
value = value.decode(sys.getdefaultencoding())
return str(value)
def execute(self, shell_cmd):
"""Run a script.
:param str shell_cmd: Command to run
:returns: `str` stdout output"""
_, out = hooks.execute(shell_cmd)
return self._normalize_string(out)
def cleanup(self, achalls): # pylint: disable=unused-argument
"""Run cleanup.sh """
if self.cleanup_script:
self.execute(self.cleanup_script)
-170
View File
@@ -1,170 +0,0 @@
"""Tests for certbot.plugins.manual."""
import os
import tempfile
import unittest
import mock
from acme import challenges
from acme import jose
from certbot import achallenges
from certbot import errors
from certbot.tests import acme_util
from certbot.tests import util as test_util
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
class AuthenticatorTest(unittest.TestCase):
"""Tests for certbot.plugins.script.Authenticator."""
def setUp(self):
from certbot.plugins.script import Authenticator
self.auth_return_value = "return from auth\n"
self.script_nonexec = create_script(b'# empty')
self.script_exec = create_script_exec(b'echo "return from auth\n"')
self.config = mock.MagicMock(
script_auth=self.script_exec,
script_cleanup=self.script_exec,
pref_challs=[challenges.Challenge.TYPES["http-01"],
challenges.Challenge.TYPES["dns-01"],
challenges.Challenge.TYPES["tls-sni-01"]])
self.tlssni_config = mock.MagicMock(
script_auth=self.script_exec,
script_cleanup=self.script_exec,
pref_challs=[challenges.Challenge.TYPES["tls-sni-01"]])
self.nochall_config = mock.MagicMock(
script_auth=self.script_exec,
script_cleanup=self.script_exec,
)
self.default = Authenticator(config=self.config, name="script")
self.onlytlssni = Authenticator(config=self.tlssni_config,
name="script")
self.nochall = Authenticator(config=self.nochall_config,
name="script")
self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)
self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY)
self.achalls = [self.http01, self.dns01]
def tearDown(self):
os.remove(self.script_exec)
os.remove(self.script_nonexec)
def test_prepare_normal(self):
"""Test prepare with typical configuration"""
from certbot.plugins.script import Authenticator
# Erroring combinations in from of (auth_script, cleanup_script, error)
for v in [("/NONEXISTENT/script.sh", "/NONEXISTENT/script.sh",
errors.HookCommandNotFound),
(self.script_nonexec, "/NONEXISTENT/script.sh",
errors.HookCommandNotFound),
(self.script_exec, "/NONEXISTENT/script.sh",
errors.HookCommandNotFound),
("/NONEXISTENT/script.sh", self.script_nonexec,
errors.HookCommandNotFound),
("/NONEXISTENT/script.sh", self.script_exec,
errors.HookCommandNotFound),
(None, self.script_exec,
errors.PluginError)]:
testconf = mock.MagicMock(
script_auth=v[0],
script_cleanup=v[1],
pref_challs=[challenges.Challenge.TYPES["http-01"]])
testauth = Authenticator(config=testconf, name="script")
self.assertRaises(v[2], testauth.prepare)
# This should not error
self.default.prepare()
self.assertEqual(len(self.default.challenges), 2)
def test_prepare_tlssni(self):
"""Test for provided, but unsupported challenge type"""
self.assertRaises(errors.PluginError, self.onlytlssni.prepare)
def test_prepare_nochall(self):
"""Test for default challenge"""
self.nochall.prepare()
self.assertEqual(len(self.nochall.challenges), 1)
def test_more_info(self):
self.assertTrue(isinstance(self.default.more_info(), str))
def test_get_chall_pref(self):
self.default.prepare()
self.assertTrue(all(issubclass(pref, challenges.Challenge)
for pref in self.default.get_chall_pref(
"foo.com")))
def test_get_supported_challenges(self):
self.default.prepare()
self.assertTrue(all(issubclass(sup, challenges.Challenge)
for sup in self.default.supported_challenges))
def test_perform(self):
resp_http = self.http01.response(KEY)
resp_dns = self.dns01.response(KEY)
self.default.prepare()
# Check for the env vars prior to the run
self.assertFalse("CERTBOT_VALIDATION" in os.environ.keys())
self.assertFalse("CERTBOT_DOMAIN" in os.environ.keys())
self.assertFalse("CERTBOT_AUTH_OUTPUT" in os.environ.keys())
pref_resp = self.default.perform(self.achalls)
self.assertEqual([resp_http, resp_dns], pref_resp)
# Check for the env vars post run
self.assertTrue("CERTBOT_VALIDATION" in os.environ.keys())
self.assertTrue("CERTBOT_DOMAIN" in os.environ.keys())
self.assertTrue("CERTBOT_AUTH_OUTPUT" in os.environ.keys())
self.assertEqual(os.environ["CERTBOT_AUTH_OUTPUT"],
self.auth_return_value.strip())
@mock.patch('certbot.plugins.script.Authenticator.execute')
def test_cleanup(self, mock_exec):
mock_exec.return_value = (0, None, None)
self.default.prepare()
self.default.cleanup(self.achalls)
self.assertEqual(mock_exec.call_count, 1)
@mock.patch('certbot.hooks.Popen')
def test_execute(self, mock_popen):
proc = mock.Mock()
# tuple values: stdout, stderr, errorcode, num_of_logger_calls
for t in [("", "", 0, 0),
(self.auth_return_value, "", 0, 0),
(None, "stderr_output", 0, 1),
("whatever", "stderr_output", 1, 2),
(b'bytestring outval', "", 0, 0)]:
proc = mock.Mock()
attrs = {'communicate.return_value': (t[0], t[1]),
'returncode': t[2]}
proc.configure_mock(**attrs) # pylint: disable=star-args
mock_popen.return_value = proc
with mock.patch('certbot.hooks.logger.error') as mock_log:
output = self.default.execute(self.script_exec)
self.assertEqual(mock_log.call_count, t[3])
self.assertTrue(isinstance(output, str))
def create_script(contents):
""" Helper to create temporary file """
f = tempfile.NamedTemporaryFile(delete=False, prefix='.sh')
f.write(contents)
f.close()
return f.name
def create_script_exec(contents):
""" Helper to create temporary file with exec permissions"""
fname = create_script(contents)
os.chmod(fname, 0o700)
return fname
+5 -5
View File
@@ -111,7 +111,8 @@ def choose_plugin(prepared, question):
while True:
disp = z_util(interfaces.IDisplay)
code, index = disp.menu(question, opts, help_label="More Info")
code, index = disp.menu(
question, opts, help_label="More Info", force_interactive=True)
if code == display_util.OK:
plugin_ep = prepared[index]
@@ -127,11 +128,12 @@ def choose_plugin(prepared, question):
msg = "Reported Error: %s" % prepared[index].prepare()
else:
msg = prepared[index].init().more_info()
z_util(interfaces.IDisplay).notification(msg)
z_util(interfaces.IDisplay).notification(msg,
force_interactive=True)
else:
return None
noninstaller_plugins = ["webroot", "manual", "standalone", "script"]
noninstaller_plugins = ["webroot", "manual", "standalone"]
def record_chosen_plugins(config, plugins, auth, inst):
"Update the config entries to reflect the plugins we actually selected."
@@ -236,8 +238,6 @@ def cli_plugin_requests(config):
req_auth = set_configurator(req_auth, "webroot")
if config.manual:
req_auth = set_configurator(req_auth, "manual")
if config.script:
req_auth = set_configurator(req_auth, "script")
logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst)
return req_auth, req_inst
+2 -1
View File
@@ -110,7 +110,8 @@ class ChoosePluginTest(unittest.TestCase):
"""Tests for certbot.plugins.selection.choose_plugin."""
def setUp(self):
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
False))
self.mock_apache = mock.Mock(
description_with_name="a", misconfigured=True)
self.mock_stand = mock.Mock(
+2 -2
View File
@@ -243,13 +243,13 @@ class Authenticator(common.Plugin):
"Could not bind TCP port {0} because you don't have "
"the appropriate permissions (for example, you "
"aren't running this program as "
"root).".format(error.port))
"root).".format(error.port), force_interactive=True)
elif error.socket_error.errno == socket.errno.EADDRINUSE:
display.notification(
"Could not bind TCP port {0} because it is already in "
"use by another process on this system (such as a web "
"server). Please stop the program in question and then "
"try again.".format(error.port))
"try again.".format(error.port), force_interactive=True)
else:
raise # XXX: How to handle unknown errors in binding?
+3 -2
View File
@@ -103,7 +103,7 @@ def already_listening_socket(port, renewer=False):
"Port {0} is already in use by another process. This will "
"prevent us from binding to that port. Please stop the "
"process that is populating the port in question and try "
"again. {1}".format(port, extra))
"again. {1}".format(port, extra), force_interactive=True)
return True
finally:
testsocket.close()
@@ -151,7 +151,8 @@ def already_listening_psutil(port, renewer=False):
"The program {0} (process ID {1}) is already listening "
"on TCP port {2}. This will prevent us from binding to "
"that port. Please stop the {0} program temporarily "
"and then try again.{3}".format(name, pid, port, extra))
"and then try again.{3}".format(name, pid, port, extra),
force_interactive=True)
return True
except (psutil.NoSuchProcess, psutil.AccessDenied):
# Perhaps the result of a race where the process could have
+5 -3
View File
@@ -45,7 +45,7 @@ to serve all files under specified web root ({0})."""
"times to handle different domains; each domain will have "
"the webroot path that preceded it. For instance: `-w "
"/var/www/example -d example.com -d www.example.com -w "
"/var/www/thing -d thing.net -d m.thing.net`")
"/var/www/thing -d thing.net -d m.thing.net` (default: Ask)")
add("map", default={}, action=_WebrootMapAction,
help="JSON dictionary mapping domains to webroot paths; this "
"implies -d for each entry. You may need to escape this from "
@@ -129,7 +129,8 @@ to serve all files under specified web root ({0})."""
"public_html or webroot directory. The webroot "
"plugin works by temporarily saving necessary "
"resources in the HTTP server's webroot directory "
"to pass domain validation challenges.")
"to pass domain validation challenges.",
force_interactive=True)
else: # code == display_util.OK
return None if index == 0 else known_webroots[index - 1]
@@ -138,7 +139,8 @@ to serve all files under specified web root ({0})."""
while True:
code, webroot = display.directory_select(
"Input the webroot for {0}:".format(domain))
"Input the webroot for {0}:".format(domain),
force_interactive=True)
if code == display_util.HELP:
# Displaying help is not currently implemented
return None
+4 -21
View File
@@ -1,7 +1,6 @@
"""Functionality for autorenewal and associated juggling of configurations"""
from __future__ import print_function
import copy
import glob
import logging
import os
import traceback
@@ -11,7 +10,6 @@ import zope.component
import OpenSSL
from certbot import configuration
from certbot import cli
from certbot import crypto_util
@@ -35,17 +33,6 @@ STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent",
INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"]
def renewal_conf_files(config):
"""Return /path/to/*.conf in the renewal conf directory"""
return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf"))
def renewal_file_for_certname(config, certname):
"""Return /path/to/certname.conf in the renewal conf directory"""
path = os.path.join(config.renewal_configs_dir, "{0}.conf".format(certname))
if not os.path.exists(path):
raise errors.CertStorageError("No certificate found with name {0}.".format(certname))
return path
def _reconstitute(config, full_path):
"""Try to instantiate a RenewableCert, updating config with relevant items.
@@ -64,8 +51,7 @@ def _reconstitute(config, full_path):
"""
try:
renewal_candidate = storage.RenewableCert(
full_path, configuration.RenewerConfiguration(config))
renewal_candidate = storage.RenewableCert(full_path, config)
except (errors.CertStorageError, IOError) as exc:
logger.warning(exc)
logger.warning("Renewal configuration file %s is broken. Skipping.", full_path)
@@ -247,9 +233,8 @@ def renew_cert(config, le_client, lineage):
new_cert = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, new_certr.body.wrapped)
new_chain = crypto_util.dump_pyopenssl_chain(new_chain)
renewal_conf = configuration.RenewerConfiguration(config.namespace)
# TODO: Check return value of save_successor
lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, renewal_conf)
lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, config)
lineage.update_all_links_to(lineage.latest_common_version())
hooks.renew_hook(config, lineage.names(), lineage.live_dir)
@@ -318,12 +303,10 @@ def handle_renewal_request(config):
"command. The renew verb may provide other options "
"for selecting certificates to renew in the future.")
renewer_config = configuration.RenewerConfiguration(config)
if config.certname:
conf_files = [renewal_file_for_certname(renewer_config, config.certname)]
conf_files = [storage.renewal_file_for_certname(config, config.certname)]
else:
conf_files = renewal_conf_files(renewer_config)
conf_files = storage.renewal_conf_files(config)
renew_successes = []
renew_failures = []
+1 -1
View File
@@ -181,7 +181,7 @@ class Reverter(object):
if for_logging:
return os.linesep.join(output)
zope.component.getUtility(interfaces.IDisplay).notification(
os.linesep.join(output))
os.linesep.join(output), force_interactive=True)
def add_to_temp_checkpoint(self, save_files, save_notes):
"""Add files to temporary checkpoint.
+131 -25
View File
@@ -1,5 +1,6 @@
"""Renewable certificates storage."""
import datetime
import glob
import logging
import os
import re
@@ -7,6 +8,7 @@ import re
import configobj
import parsedatetime
import pytz
import shutil
import six
import certbot
@@ -20,9 +22,22 @@ from certbot import util
logger = logging.getLogger(__name__)
ALL_FOUR = ("cert", "privkey", "chain", "fullchain")
README = "README"
CURRENT_VERSION = util.get_strict_version(certbot.__version__)
def renewal_conf_files(config):
"""Return /path/to/*.conf in the renewal conf directory"""
return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf"))
def renewal_file_for_certname(config, certname):
"""Return /path/to/certname.conf in the renewal conf directory"""
path = os.path.join(config.renewal_configs_dir, "{0}.conf".format(certname))
if not os.path.exists(path):
raise errors.CertStorageError("No certificate found with name {0} (expected "
"{1}).".format(certname, path))
return path
def config_with_defaults(config=None):
"""Merge supplied config, if provided, on top of builtin defaults."""
defaults_copy = configobj.ConfigObj(constants.RENEWER_DEFAULTS)
@@ -99,13 +114,11 @@ def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_d
def rename_renewal_config(prev_name, new_name, cli_config):
"""Renames cli_config.certname's config to cli_config.new_certname.
:param .RenewerConfiguration cli_config: parsed command line
:param .NamespaceConfig cli_config: parsed command line
arguments
"""
prev_filename = os.path.join(
cli_config.renewal_configs_dir, prev_name) + ".conf"
new_filename = os.path.join(
cli_config.renewal_configs_dir, new_name) + ".conf"
prev_filename = renewal_filename_for_lineagename(cli_config, prev_name)
new_filename = renewal_filename_for_lineagename(cli_config, new_name)
if os.path.exists(new_filename):
raise errors.ConfigurationError("The new certificate name "
"is already in use.")
@@ -122,15 +135,14 @@ def update_configuration(lineagename, archive_dir, target, cli_config):
:param str lineagename: Name of the lineage being modified
:param str archive_dir: Absolute path to the archive directory
:param dict target: Maps ALL_FOUR to their symlink paths
:param .RenewerConfiguration cli_config: parsed command line
:param .NamespaceConfig cli_config: parsed command line
arguments
:returns: Configuration object for the updated config file
:rtype: configobj.ConfigObj
"""
config_filename = os.path.join(
cli_config.renewal_configs_dir, lineagename) + ".conf"
config_filename = renewal_filename_for_lineagename(cli_config, lineagename)
temp_filename = config_filename + ".new"
# If an existing tempfile exists, delete it
@@ -198,6 +210,98 @@ def lineagename_for_filename(config_filename):
"renewal config file name must end in .conf")
return os.path.basename(config_filename[:-len(".conf")])
def renewal_filename_for_lineagename(config, lineagename):
"""Returns the lineagename for a configuration filename.
"""
return os.path.join(config.renewal_configs_dir, lineagename) + ".conf"
def _relpath_from_file(archive_dir, from_file):
"""Path to a directory from a file"""
return os.path.relpath(archive_dir, os.path.dirname(from_file))
def _full_archive_path(config_obj, cli_config, lineagename):
"""Returns the full archive path for a lineagename
Uses cli_config to determine archive path if not available from config_obj.
:param configobj.ConfigObj config_obj: Renewal conf file contents (can be None)
:param configuration.NamespaceConfig cli_config: Main config file
:param str lineagename: Certificate name
"""
if config_obj and "archive_dir" in config_obj:
return config_obj["archive_dir"]
else:
return os.path.join(cli_config.default_archive_dir, lineagename)
def _full_live_path(cli_config, lineagename):
"""Returns the full default live path for a lineagename"""
return os.path.join(cli_config.live_dir, lineagename)
def delete_files(config, certname):
"""Delete all files related to the certificate.
If some files are not found, ignore them and continue.
"""
renewal_filename = renewal_file_for_certname(config, certname)
# file exists
full_default_archive_dir = _full_archive_path(None, config, certname)
full_default_live_dir = _full_live_path(config, certname)
try:
renewal_config = configobj.ConfigObj(renewal_filename)
except configobj.ConfigObjError:
# config is corrupted
logger.warning("Could not parse %s. You may wish to manually "
"delete the contents of %s and %s.", renewal_filename,
full_default_live_dir, full_default_archive_dir)
raise errors.CertStorageError(
"error parsing {0}".format(renewal_filename))
finally:
# we couldn't read it, but let's at least delete it
# if this was going to fail, it already would have.
os.remove(renewal_filename)
logger.debug("Removed %s", renewal_filename)
# cert files and (hopefully) live directory
# it's not guaranteed that the files are in our default storage
# structure. so, first delete the cert files.
directory_names = set()
for kind in ALL_FOUR:
link = renewal_config.get(kind)
try:
os.remove(link)
logger.debug("Removed %s", link)
except OSError:
logger.debug("Unable to delete %s", link)
directory = os.path.dirname(link)
directory_names.add(directory)
# if all four were in the same directory, and the only thing left
# is the README file (or nothing), delete that directory.
# this will be wrong in very few but some cases.
if len(directory_names) == 1:
# delete the README file
directory = directory_names.pop()
readme_path = os.path.join(directory, README)
try:
os.remove(readme_path)
logger.debug("Removed %s", readme_path)
except OSError:
logger.debug("Unable to delete %s", readme_path)
# if it's now empty, delete the directory
try:
os.rmdir(directory) # only removes empty directories
logger.debug("Removed %s", directory)
except OSError:
logger.debug("Unable to remove %s; may not be empty.", directory)
# archive directory
try:
archive_path = _full_archive_path(renewal_config, config, certname)
shutil.rmtree(archive_path)
logger.debug("Removed %s", archive_path)
except OSError:
logger.debug("Unable to remove %s", archive_path)
class RenewableCert(object):
# pylint: disable=too-many-instance-attributes,too-many-public-methods
@@ -240,7 +344,7 @@ class RenewableCert(object):
:param str config_filename: the path to the renewal config file
that defines this lineage.
:param .RenewerConfiguration: parsed command line arguments
:param .NamespaceConfig: parsed command line arguments
:raises .CertStorageError: if the configuration file's name didn't end
in ".conf", or the file is missing or broken.
@@ -299,11 +403,15 @@ class RenewableCert(object):
@property
def archive_dir(self):
"""Returns the default or specified archive directory"""
if "archive_dir" in self.configuration:
return self.configuration["archive_dir"]
else:
return os.path.join(
self.cli_config.default_archive_dir, self.lineagename)
return _full_archive_path(self.configuration,
self.cli_config, self.lineagename)
def relative_archive_dir(self, from_file):
"""Returns the default or specified archive directory as a relative path
Used for creating symbolic links.
"""
return _relpath_from_file(self.archive_dir, from_file)
@property
def is_test_cert(self):
@@ -331,7 +439,8 @@ class RenewableCert(object):
for kind in ALL_FOUR:
link = getattr(self, kind)
previous_link = get_link_target(link)
new_link = os.path.join(self.archive_dir, os.path.basename(previous_link))
new_link = os.path.join(self.relative_archive_dir(link),
os.path.basename(previous_link))
os.unlink(link)
os.symlink(new_link, link)
@@ -815,7 +924,7 @@ class RenewableCert(object):
:param str cert: the initial certificate version in PEM format
:param str privkey: the private key in PEM format
:param str chain: the certificate chain in PEM format
:param .RenewerConfiguration cli_config: parsed command line
:param .NamespaceConfig cli_config: parsed command line
arguments
:returns: the newly-created RenewalCert object
@@ -831,16 +940,13 @@ class RenewableCert(object):
logger.debug("Creating directory %s.", i)
config_file, config_filename = util.unique_lineage_name(
cli_config.renewal_configs_dir, lineagename)
if not config_filename.endswith(".conf"):
raise errors.CertStorageError(
"renewal config file name must end in .conf")
# Determine where on disk everything will go
# lineagename will now potentially be modified based on which
# renewal configuration file could actually be created
lineagename = os.path.basename(config_filename)[:-len(".conf")]
archive = os.path.join(cli_config.default_archive_dir, lineagename)
live_dir = os.path.join(cli_config.live_dir, lineagename)
lineagename = lineagename_for_filename(config_filename)
archive = _full_archive_path(None, cli_config, lineagename)
live_dir = _full_live_path(cli_config, lineagename)
if os.path.exists(archive):
raise errors.CertStorageError(
"archive directory exists for " + lineagename)
@@ -856,7 +962,7 @@ class RenewableCert(object):
target = dict([(kind, os.path.join(live_dir, kind + ".pem"))
for kind in ALL_FOUR])
for kind in ALL_FOUR:
os.symlink(os.path.join(archive, kind + "1.pem"),
os.symlink(os.path.join(_relpath_from_file(archive, target[kind]), kind + "1.pem"),
target[kind])
with open(target["cert"], "wb") as f:
logger.debug("Writing certificate to %s.", target["cert"])
@@ -875,7 +981,7 @@ class RenewableCert(object):
f.write(cert + chain)
# Write a README file to the live directory
readme_path = os.path.join(live_dir, "README")
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"
@@ -916,7 +1022,7 @@ class RenewableCert(object):
:param str new_privkey: the new private key, in PEM format,
or ``None``, if the private key has not changed
:param str new_chain: the new chain, in PEM format
:param .RenewerConfiguration cli_config: parsed command line
:param .NamespaceConfig cli_config: parsed command line
arguments
:returns: the new version number that was created
+11
View File
@@ -7,9 +7,12 @@ from acme import challenges
from acme import jose
from acme import messages
from certbot import auth_handler
from certbot.tests import util
JWK = jose.JWK.load(util.load_vector('rsa512_key.pem'))
KEY = util.load_rsa_private_key('rsa512_key.pem')
# Challenges
@@ -50,6 +53,14 @@ DNS01_P = chall_to_challb(DNS01, messages.STATUS_PENDING)
CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS01_P]
# AnnotatedChallenge objects
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")
ACHALLENGES = [HTTP01_A, TLSSNI01_A, DNS01_A]
def gen_authzr(authz_status, domain, challs, statuses, combos=True):
"""Generate an authorization resource.
+94 -48
View File
@@ -1,6 +1,7 @@
"""Tests for certbot.cert_manager."""
# pylint disable=protected-access
# pylint: disable=protected-access
import os
import re
import shutil
import tempfile
import unittest
@@ -25,12 +26,12 @@ class BaseCertManagerTest(unittest.TestCase):
os.makedirs(os.path.join(self.tempdir, "renewal"))
self.cli_config = mock.MagicMock(
self.cli_config = configuration.NamespaceConfig(mock.MagicMock(
config_dir=self.tempdir,
work_dir=self.tempdir,
logs_dir=self.tempdir,
quiet=False,
)
))
self.domains = {
"example.org": None,
@@ -47,7 +48,7 @@ class BaseCertManagerTest(unittest.TestCase):
junk.close()
def _set_up_config(self, domain, custom_archive):
# TODO: maybe provide RenewerConfiguration.make_dirs?
# TODO: maybe provide NamespaceConfig.make_dirs?
# TODO: main() should create those dirs, c.f. #902
os.makedirs(os.path.join(self.tempdir, "live", domain))
config = configobj.ConfigObj()
@@ -99,10 +100,31 @@ class UpdateLiveSymlinksTest(BaseCertManagerTest):
cert_manager.update_live_symlinks(self.cli_config)
# check that symlinks go where they should
for domain in self.domains:
for kind in ALL_FOUR:
self.assertEqual(os.readlink(self.configs[domain][kind]),
archive_paths[domain][kind])
prev_dir = os.getcwd()
try:
for domain in self.domains:
for kind in ALL_FOUR:
os.chdir(os.path.dirname(self.configs[domain][kind]))
self.assertEqual(
os.path.realpath(os.readlink(self.configs[domain][kind])),
os.path.realpath(archive_paths[domain][kind]))
finally:
os.chdir(prev_dir)
class DeleteTest(storage_test.BaseRenewableCertTest):
"""Tests for certbot.cert_manager.delete
"""
@mock.patch('zope.component.getUtility')
@mock.patch('certbot.cert_manager.lineage_for_certname')
@mock.patch('certbot.storage.delete_files')
def test_delete(self, mock_delete_files, mock_lineage_for_certname, unused_get_utility):
"""Test delete"""
mock_lineage_for_certname.return_value = self.test_rc
self.cli_config.certname = "example.org"
from certbot import cert_manager
cert_manager.delete(self.cli_config)
self.assertTrue(mock_delete_files.called)
class CertificatesTest(BaseCertManagerTest):
@@ -145,12 +167,12 @@ class CertificatesTest(BaseCertManagerTest):
def test_certificates_no_files(self, mock_utility, mock_logger):
tempdir = tempfile.mkdtemp()
cli_config = mock.MagicMock(
cli_config = configuration.NamespaceConfig(mock.MagicMock(
config_dir=tempdir,
work_dir=tempdir,
logs_dir=tempdir,
quiet=False,
)
))
os.makedirs(os.path.join(tempdir, "renewal"))
self._certificates(cli_config)
@@ -158,7 +180,9 @@ class CertificatesTest(BaseCertManagerTest):
self.assertTrue(mock_utility.called)
shutil.rmtree(tempdir)
def test_report_human_readable(self):
@mock.patch('certbot.cert_manager.ocsp.RevocationChecker.ocsp_revoked')
def test_report_human_readable(self, mock_revoked):
mock_revoked.return_value = None
from certbot import cert_manager
import datetime, pytz
expiry = pytz.UTC.fromutc(datetime.datetime.utcnow())
@@ -168,118 +192,140 @@ class CertificatesTest(BaseCertManagerTest):
cert.names.return_value = ["nameone", "nametwo"]
cert.is_test_cert = False
parsed_certs = [cert]
# pylint: disable=protected-access
out = cert_manager._report_human_readable(parsed_certs)
get_report = lambda: cert_manager._report_human_readable(mock_config, parsed_certs)
mock_config = mock.MagicMock(certname=None, lineagename=None)
# pylint: disable=protected-access
out = get_report()
self.assertTrue("INVALID: EXPIRED" in out)
cert.target_expiry += datetime.timedelta(hours=2)
# pylint: disable=protected-access
out = cert_manager._report_human_readable(parsed_certs)
out = get_report()
self.assertTrue('1 hour(s)' in out)
self.assertTrue('VALID' in out and not 'INVALID' in out)
cert.target_expiry += datetime.timedelta(days=1)
# pylint: disable=protected-access
out = cert_manager._report_human_readable(parsed_certs)
out = get_report()
self.assertTrue('1 day' in out)
self.assertFalse('under' in out)
self.assertTrue('VALID' in out and not 'INVALID' in out)
cert.target_expiry += datetime.timedelta(days=2)
# pylint: disable=protected-access
out = cert_manager._report_human_readable(parsed_certs)
out = get_report()
self.assertTrue('3 days' in out)
self.assertTrue('VALID' in out and not 'INVALID' in out)
cert.is_test_cert = True
out = cert_manager._report_human_readable(parsed_certs)
self.assertTrue('INVALID: TEST CERT' in out)
mock_revoked.return_value = True
out = get_report()
self.assertTrue('INVALID: TEST_CERT, REVOKED' in out)
cert = mock.MagicMock(lineagename="indescribable")
cert.target_expiry = expiry
cert.names.return_value = ["nameone", "thrice.named"]
cert.is_test_cert = True
parsed_certs.append(cert)
out = get_report()
self.assertEqual(len(re.findall("INVALID:", out)), 2)
mock_config.domains = ["thrice.named"]
out = get_report()
self.assertEqual(len(re.findall("INVALID:", out)), 1)
mock_config.domains = ["nameone"]
out = get_report()
self.assertEqual(len(re.findall("INVALID:", out)), 2)
mock_config.certname = "indescribable"
out = get_report()
self.assertEqual(len(re.findall("INVALID:", out)), 1)
mock_config.certname = "horror"
out = get_report()
self.assertEqual(len(re.findall("INVALID:", out)), 0)
class SearchLineagesTest(unittest.TestCase):
class SearchLineagesTest(BaseCertManagerTest):
"""Tests for certbot.cert_manager._search_lineages."""
@mock.patch('certbot.configuration.RenewerConfiguration')
@mock.patch('certbot.util.make_or_verify_dir')
@mock.patch('certbot.renewal.renewal_conf_files')
@mock.patch('certbot.storage.renewal_conf_files')
@mock.patch('certbot.storage.RenewableCert')
def test_cert_storage_error(self, mock_renewable_cert, mock_renewal_conf_files,
mock_make_or_verify_dir, mock_renewer_config):
mock_make_or_verify_dir):
mock_renewal_conf_files.return_value = ["badfile"]
mock_renewable_cert.side_effect = errors.CertStorageError
from certbot import cert_manager
# pylint: disable=protected-access
self.assertEqual(cert_manager._search_lineages(None, lambda x: x, "check"), "check")
self.assertEqual(cert_manager._search_lineages(self.cli_config, lambda x: x, "check"),
"check")
self.assertTrue(mock_make_or_verify_dir.called)
self.assertTrue(mock_renewer_config)
class LineageForCertnameTest(unittest.TestCase):
class LineageForCertnameTest(BaseCertManagerTest):
"""Tests for certbot.cert_manager.lineage_for_certname"""
@mock.patch('certbot.configuration.RenewerConfiguration')
@mock.patch('certbot.util.make_or_verify_dir')
@mock.patch('certbot.renewal.renewal_conf_files')
@mock.patch('certbot.storage.renewal_conf_files')
@mock.patch('certbot.storage.RenewableCert')
def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files,
mock_make_or_verify_dir, mock_renewer_config):
mock_make_or_verify_dir):
mock_renewal_conf_files.return_value = ["somefile.conf"]
mock_match = mock.Mock(lineagename="example.com")
mock_renewable_cert.return_value = mock_match
from certbot import cert_manager
self.assertEqual(cert_manager.lineage_for_certname(None, "example.com"), mock_match)
self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
mock_match)
self.assertTrue(mock_make_or_verify_dir.called)
self.assertTrue(mock_renewer_config)
@mock.patch('certbot.configuration.RenewerConfiguration')
@mock.patch('certbot.util.make_or_verify_dir')
@mock.patch('certbot.renewal.renewal_conf_files')
@mock.patch('certbot.storage.renewal_conf_files')
@mock.patch('certbot.storage.RenewableCert')
def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files,
mock_make_or_verify_dir, mock_renewer_config):
mock_make_or_verify_dir):
mock_renewal_conf_files.return_value = ["somefile.conf"]
mock_match = mock.Mock(lineagename="other.com")
mock_renewable_cert.return_value = mock_match
from certbot import cert_manager
self.assertEqual(cert_manager.lineage_for_certname(None, "example.com"), None)
self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
None)
self.assertTrue(mock_make_or_verify_dir.called)
self.assertTrue(mock_renewer_config)
class DomainsForCertnameTest(unittest.TestCase):
class DomainsForCertnameTest(BaseCertManagerTest):
"""Tests for certbot.cert_manager.domains_for_certname"""
@mock.patch('certbot.configuration.RenewerConfiguration')
@mock.patch('certbot.util.make_or_verify_dir')
@mock.patch('certbot.renewal.renewal_conf_files')
@mock.patch('certbot.storage.renewal_conf_files')
@mock.patch('certbot.storage.RenewableCert')
def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files,
mock_make_or_verify_dir, mock_renewer_config):
mock_make_or_verify_dir):
mock_renewal_conf_files.return_value = ["somefile.conf"]
mock_match = mock.Mock(lineagename="example.com")
domains = ["example.com", "example.org"]
mock_match.names.return_value = domains
mock_renewable_cert.return_value = mock_match
from certbot import cert_manager
self.assertEqual(cert_manager.domains_for_certname(None, "example.com"), domains)
self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "example.com"),
domains)
self.assertTrue(mock_make_or_verify_dir.called)
self.assertTrue(mock_renewer_config)
@mock.patch('certbot.configuration.RenewerConfiguration')
@mock.patch('certbot.util.make_or_verify_dir')
@mock.patch('certbot.renewal.renewal_conf_files')
@mock.patch('certbot.storage.renewal_conf_files')
@mock.patch('certbot.storage.RenewableCert')
def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files,
mock_make_or_verify_dir, mock_renewer_config):
mock_make_or_verify_dir):
mock_renewal_conf_files.return_value = ["somefile.conf"]
mock_match = mock.Mock(lineagename="example.com")
domains = ["example.com", "example.org"]
mock_match.names.return_value = domains
mock_renewable_cert.return_value = mock_match
from certbot import cert_manager
self.assertEqual(cert_manager.domains_for_certname(None, "other.com"), None)
self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "other.com"),
None)
self.assertTrue(mock_make_or_verify_dir.called)
self.assertTrue(mock_renewer_config)
class RenameLineageTest(BaseCertManagerTest):
@@ -287,7 +333,7 @@ class RenameLineageTest(BaseCertManagerTest):
def setUp(self):
super(RenameLineageTest, self).setUp()
self.mock_config = configuration.RenewerConfiguration(
self.mock_config = configuration.NamespaceConfig(
namespace=mock.MagicMock(
config_dir=self.tempdir,
work_dir=self.tempdir,
@@ -301,7 +347,7 @@ class RenameLineageTest(BaseCertManagerTest):
from certbot import cert_manager
return cert_manager.rename_lineage(*args, **kwargs)
@mock.patch('certbot.renewal.renewal_conf_files')
@mock.patch('certbot.storage.renewal_conf_files')
@mock.patch('certbot.main.zope.component.getUtility')
def test_no_certname(self, mock_get_utility, mock_renewal_conf_files):
mock_config = mock.Mock(certname=None, new_certname="two")
@@ -377,7 +423,7 @@ class RenameLineageTest(BaseCertManagerTest):
mock_config.new_certname = "example.org"
self.assertRaises(errors.ConfigurationError, self._call, mock_config)
mock_config.new_certname = "one/two"
mock_config.new_certname = "one{0}two".format(os.path.sep)
self.assertRaises(errors.ConfigurationError, self._call, mock_config)
+24 -14
View File
@@ -81,15 +81,17 @@ class ParseTest(unittest.TestCase):
out = self._help_output(['--help', 'all'])
self.assertTrue("--configurator" in out)
self.assertTrue("how a cert is deployed" in out)
self.assertTrue("--manual-test-mode" in out)
self.assertTrue("--webroot-path" in out)
self.assertTrue("--text" not in out)
self.assertTrue("--dialog" not in out)
self.assertTrue("%s" not in out)
self.assertTrue("{0}" not in out)
out = self._help_output(['-h', 'nginx'])
if "nginx" in self.plugins:
# may be false while building distributions without plugins
self.assertTrue("--nginx-ctl" in out)
self.assertTrue("--manual-test-mode" not in out)
self.assertTrue("--webroot-path" not in out)
self.assertTrue("--checkpoints" not in out)
out = self._help_output(['-h'])
@@ -97,10 +99,10 @@ class ParseTest(unittest.TestCase):
if "nginx" in self.plugins:
self.assertTrue("Use the Nginx plugin" in out)
else:
self.assertTrue("(nginx support is experimental" in out)
self.assertTrue("(the certbot nginx plugin is not" in out)
out = self._help_output(['--help', 'plugins'])
self.assertTrue("--manual-test-mode" not in out)
self.assertTrue("--webroot-path" not in out)
self.assertTrue("--prepare" in out)
self.assertTrue('"plugins" subcommand' in out)
@@ -125,8 +127,10 @@ class ParseTest(unittest.TestCase):
self.assertTrue("--key-path" not in out)
out = self._help_output(['-h'])
self.assertTrue(cli.usage_strings(self.plugins)[0] in out)
self.assertTrue(cli.SHORT_USAGE in out)
self.assertTrue(cli.COMMAND_OVERVIEW[:100] in out)
self.assertTrue("%s" not in out)
self.assertTrue("{0}" not in out)
def test_parse_domains(self):
short_args = ['-d', 'example.com']
@@ -258,6 +262,12 @@ class ParseTest(unittest.TestCase):
self.assertFalse(cli.option_was_set(
config_dir_option, cli.flag_default(config_dir_option)))
def test_force_interactive(self):
self.assertRaises(
errors.Error, self.parse, "renew --force-interactive".split())
self.assertRaises(
errors.Error, self.parse, "-n --force-interactive".split())
class DefaultTest(unittest.TestCase):
"""Tests for certbot.cli._Default."""
@@ -295,22 +305,22 @@ class SetByCliTest(unittest.TestCase):
def test_report_config_interaction_str(self):
cli.report_config_interaction('manual_public_ip_logging_ok',
'manual_test_mode')
cli.report_config_interaction('manual_test_mode', 'manual')
'manual_auth_hook')
cli.report_config_interaction('manual_auth_hook', 'manual')
self._test_report_config_interaction_common()
def test_report_config_interaction_iterable(self):
cli.report_config_interaction(('manual_public_ip_logging_ok',),
('manual_test_mode',))
cli.report_config_interaction(('manual_test_mode',), ('manual',))
('manual_auth_hook',))
cli.report_config_interaction(('manual_auth_hook',), ('manual',))
self._test_report_config_interaction_common()
def _test_report_config_interaction_common(self):
"""Tests implied interaction between manual flags.
--manual implies --manual-test-mode which implies
--manual implies --manual-auth-hook which implies
--manual-public-ip-logging-ok. These interactions don't actually
exist in the client, but are used here for testing purposes.
@@ -318,13 +328,13 @@ class SetByCliTest(unittest.TestCase):
args = ['--manual']
verb = 'renew'
for v in ('manual', 'manual_test_mode', 'manual_public_ip_logging_ok'):
for v in ('manual', 'manual_auth_hook', 'manual_public_ip_logging_ok'):
self.assertTrue(_call_set_by_cli(v, args, verb))
cli.set_by_cli.detector = None
args = ['--manual-test-mode']
for v in ('manual_test_mode', 'manual_public_ip_logging_ok'):
args = ['--manual-auth-hook', 'command']
for v in ('manual_auth_hook', 'manual_public_ip_logging_ok'):
self.assertTrue(_call_set_by_cli(v, args, verb))
self.assertFalse(_call_set_by_cli('manual', args, verb))
+3 -16
View File
@@ -88,31 +88,19 @@ class NamespaceConfigTest(unittest.TestCase):
self.assertTrue(os.path.isabs(config.key_dir))
self.assertTrue(os.path.isabs(config.temp_checkpoint_dir))
class RenewerConfigurationTest(unittest.TestCase):
"""Test for certbot.configuration.RenewerConfiguration."""
def setUp(self):
self.namespace = mock.MagicMock(config_dir='/tmp/config')
from certbot.configuration import RenewerConfiguration
self.config = RenewerConfiguration(self.namespace)
@mock.patch('certbot.configuration.constants')
def test_dynamic_dirs(self, constants):
def test_renewal_dynamic_dirs(self, constants):
constants.ARCHIVE_DIR = 'a'
constants.LIVE_DIR = 'l'
constants.RENEWAL_CONFIGS_DIR = 'renewal_configs'
constants.RENEWER_CONFIG_FILENAME = 'r.conf'
self.assertEqual(self.config.default_archive_dir, '/tmp/config/a')
self.assertEqual(self.config.live_dir, '/tmp/config/l')
self.assertEqual(
self.config.renewal_configs_dir, '/tmp/config/renewal_configs')
self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf')
def test_absolute_paths(self):
def test_renewal_absolute_paths(self):
from certbot.configuration import NamespaceConfig
from certbot.configuration import RenewerConfiguration
config_base = "foo"
work_base = "bar"
@@ -125,12 +113,11 @@ class RenewerConfigurationTest(unittest.TestCase):
mock_namespace.config_dir = config_base
mock_namespace.work_dir = work_base
mock_namespace.logs_dir = logs_base
config = RenewerConfiguration(NamespaceConfig(mock_namespace))
config = NamespaceConfig(mock_namespace)
self.assertTrue(os.path.isabs(config.default_archive_dir))
self.assertTrue(os.path.isabs(config.live_dir))
self.assertTrue(os.path.isabs(config.renewal_configs_dir))
self.assertTrue(os.path.isabs(config.renewer_config_file))
if __name__ == '__main__':
+8 -4
View File
@@ -84,7 +84,8 @@ class GetEmailTest(unittest.TestCase):
class ChooseAccountTest(unittest.TestCase):
"""Tests for certbot.display.ops.choose_account."""
def setUp(self):
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
False))
self.accounts_dir = tempfile.mkdtemp("accounts")
self.account_keys_dir = os.path.join(self.accounts_dir, "keys")
@@ -127,7 +128,8 @@ class ChooseAccountTest(unittest.TestCase):
class GenSSLLabURLs(unittest.TestCase):
"""Loose test of _gen_ssl_lab_urls. URL can change easily in the future."""
def setUp(self):
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
False))
@classmethod
def _call(cls, domains):
@@ -146,7 +148,8 @@ class GenSSLLabURLs(unittest.TestCase):
class GenHttpsNamesTest(unittest.TestCase):
"""Test _gen_https_names."""
def setUp(self):
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
False))
@classmethod
def _call(cls, domains):
@@ -193,7 +196,8 @@ class GenHttpsNamesTest(unittest.TestCase):
class ChooseNamesTest(unittest.TestCase):
"""Test choose names."""
def setUp(self):
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
False))
self.mock_install = mock.MagicMock()
@classmethod
+130 -37
View File
@@ -1,10 +1,12 @@
"""Test :mod:`certbot.display.util`."""
import inspect
import os
import unittest
import mock
import certbot.errors as errors
from certbot import errors
from certbot import interfaces
from certbot.display import util as display_util
@@ -20,10 +22,11 @@ class FileOutputDisplayTest(unittest.TestCase):
functions look to a user, uncomment the test_visual function.
"""
# pylint:disable=too-many-public-methods
def setUp(self):
super(FileOutputDisplayTest, self).setUp()
self.mock_stdout = mock.MagicMock()
self.displayer = display_util.FileDisplay(self.mock_stdout)
self.displayer = display_util.FileDisplay(self.mock_stdout, False)
def test_notification_no_pause(self):
self.displayer.notification("message", False)
@@ -33,79 +36,128 @@ class FileOutputDisplayTest(unittest.TestCase):
def test_notification_pause(self):
with mock.patch("six.moves.input", return_value="enter"):
self.displayer.notification("message")
self.displayer.notification("message", force_interactive=True)
self.assertTrue("message" in self.mock_stdout.write.call_args[0][0])
def test_notification_noninteractive(self):
self._force_noninteractive(self.displayer.notification, "message")
string = self.mock_stdout.write.call_args[0][0]
self.assertTrue("message" in string)
def test_notification_noninteractive2(self):
# The main purpose of this test is to make sure we only call
# logger.warning once which _force_noninteractive checks internally
self._force_noninteractive(self.displayer.notification, "message")
string = self.mock_stdout.write.call_args[0][0]
self.assertTrue("message" in string)
self.assertTrue(self.displayer.skipped_interaction)
self._force_noninteractive(self.displayer.notification, "message2")
string = self.mock_stdout.write.call_args[0][0]
self.assertTrue("message2" in string)
@mock.patch("certbot.display.util."
"FileDisplay._get_valid_int_ans")
def test_menu(self, mock_ans):
mock_ans.return_value = (display_util.OK, 1)
ret = self.displayer.menu("message", CHOICES)
ret = self.displayer.menu("message", CHOICES, force_interactive=True)
self.assertEqual(ret, (display_util.OK, 0))
def test_menu_noninteractive(self):
default = 0
result = self._force_noninteractive(
self.displayer.menu, "msg", CHOICES, default=default)
self.assertEqual(result, (display_util.OK, default))
def test_input_cancel(self):
with mock.patch("six.moves.input", return_value="c"):
code, _ = self.displayer.input("message")
code, _ = self.displayer.input("message", force_interactive=True)
self.assertTrue(code, display_util.CANCEL)
def test_input_normal(self):
with mock.patch("six.moves.input", return_value="domain.com"):
code, input_ = self.displayer.input("message")
code, input_ = self.displayer.input("message", force_interactive=True)
self.assertEqual(code, display_util.OK)
self.assertEqual(input_, "domain.com")
def test_input_noninteractive(self):
default = "foo"
code, input_ = self._force_noninteractive(
self.displayer.input, "message", default=default)
self.assertEqual(code, display_util.OK)
self.assertEqual(input_, default)
def test_input_assertion_fail(self):
self.assertRaises(AssertionError, self._force_noninteractive,
self.displayer.input, "message", cli_flag="--flag")
def test_yesno(self):
with mock.patch("six.moves.input", return_value="Yes"):
self.assertTrue(self.displayer.yesno("message"))
self.assertTrue(self.displayer.yesno(
"message", force_interactive=True))
with mock.patch("six.moves.input", return_value="y"):
self.assertTrue(self.displayer.yesno("message"))
self.assertTrue(self.displayer.yesno(
"message", force_interactive=True))
with mock.patch("six.moves.input", side_effect=["maybe", "y"]):
self.assertTrue(self.displayer.yesno("message"))
self.assertTrue(self.displayer.yesno(
"message", force_interactive=True))
with mock.patch("six.moves.input", return_value="No"):
self.assertFalse(self.displayer.yesno("message"))
self.assertFalse(self.displayer.yesno(
"message", force_interactive=True))
with mock.patch("six.moves.input", side_effect=["cancel", "n"]):
self.assertFalse(self.displayer.yesno("message"))
self.assertFalse(self.displayer.yesno(
"message", force_interactive=True))
with mock.patch("six.moves.input", return_value="a"):
self.assertTrue(self.displayer.yesno("msg", yes_label="Agree"))
self.assertTrue(self.displayer.yesno(
"msg", yes_label="Agree", force_interactive=True))
@mock.patch("certbot.display.util.FileDisplay.input")
def test_yesno_noninteractive(self):
self.assertTrue(self._force_noninteractive(
self.displayer.yesno, "message", default=True))
@mock.patch("certbot.display.util.six.moves.input")
def test_checklist_valid(self, mock_input):
mock_input.return_value = (display_util.OK, "2 1")
code, tag_list = self.displayer.checklist("msg", TAGS)
mock_input.return_value = "2 1"
code, tag_list = self.displayer.checklist(
"msg", TAGS, force_interactive=True)
self.assertEqual(
(code, set(tag_list)), (display_util.OK, set(["tag1", "tag2"])))
@mock.patch("certbot.display.util.FileDisplay.input")
@mock.patch("certbot.display.util.six.moves.input")
def test_checklist_empty(self, mock_input):
mock_input.return_value = (display_util.OK, "")
code, tag_list = self.displayer.checklist("msg", TAGS)
mock_input.return_value = ""
code, tag_list = self.displayer.checklist("msg", TAGS, force_interactive=True)
self.assertEqual(
(code, set(tag_list)), (display_util.OK, set(["tag1", "tag2", "tag3"])))
@mock.patch("certbot.display.util.FileDisplay.input")
@mock.patch("certbot.display.util.six.moves.input")
def test_checklist_miss_valid(self, mock_input):
mock_input.side_effect = [
(display_util.OK, "10"),
(display_util.OK, "tag1 please"),
(display_util.OK, "1")
]
mock_input.side_effect = ["10", "tag1 please", "1"]
ret = self.displayer.checklist("msg", TAGS)
ret = self.displayer.checklist("msg", TAGS, force_interactive=True)
self.assertEqual(ret, (display_util.OK, ["tag1"]))
@mock.patch("certbot.display.util.FileDisplay.input")
@mock.patch("certbot.display.util.six.moves.input")
def test_checklist_miss_quit(self, mock_input):
mock_input.side_effect = [
(display_util.OK, "10"),
(display_util.CANCEL, "1")
]
ret = self.displayer.checklist("msg", TAGS)
mock_input.side_effect = ["10", "c"]
ret = self.displayer.checklist("msg", TAGS, force_interactive=True)
self.assertEqual(ret, (display_util.CANCEL, []))
def test_checklist_noninteractive(self):
default = TAGS
code, input_ = self._force_noninteractive(
self.displayer.checklist, "msg", TAGS, default=default)
self.assertEqual(code, display_util.OK)
self.assertEqual(input_, default)
def test_scrub_checklist_input_valid(self):
# pylint: disable=protected-access
indices = [
@@ -123,14 +175,38 @@ class FileOutputDisplayTest(unittest.TestCase):
self.displayer._scrub_checklist_input(list_, TAGS))
self.assertEqual(set_tags, exp[i])
@mock.patch("certbot.display.util.FileDisplay.input")
@mock.patch("certbot.display.util.six.moves.input")
def test_directory_select(self, mock_input):
message = "msg"
result = (display_util.OK, "/var/www/html",)
mock_input.return_value = result
# pylint: disable=star-args
args = ["msg", "/var/www/html", "--flag", True]
user_input = "/var/www/html"
mock_input.return_value = user_input
self.assertEqual(self.displayer.directory_select(message), result)
mock_input.assert_called_once_with(message)
returned = self.displayer.directory_select(*args)
self.assertEqual(returned, (display_util.OK, user_input))
def test_directory_select_noninteractive(self):
default = "/var/www/html"
code, input_ = self._force_noninteractive(
self.displayer.directory_select, "msg", default=default)
self.assertEqual(code, display_util.OK)
self.assertEqual(input_, default)
def _force_noninteractive(self, func, *args, **kwargs):
skipped_interaction = self.displayer.skipped_interaction
with mock.patch("certbot.display.util.sys.stdin") as mock_stdin:
mock_stdin.isatty.return_value = False
with mock.patch("certbot.display.util.logger") as mock_logger:
result = func(*args, **kwargs)
if skipped_interaction:
self.assertFalse(mock_logger.warning.called)
else:
self.assertEqual(mock_logger.warning.call_count, 1)
return result
def test_scrub_checklist_input_invalid(self):
# pylint: disable=protected-access
@@ -185,6 +261,13 @@ class FileOutputDisplayTest(unittest.TestCase):
self.displayer._get_valid_int_ans(3),
(display_util.CANCEL, -1))
def test_methods_take_force_interactive(self):
# Every IDisplay method implemented by FileDisplay must take
# force_interactive to prevent workflow regressions.
for name in interfaces.IDisplay.names(): # pylint: disable=no-member
arg_spec = inspect.getargspec(getattr(self.displayer, name))
self.assertTrue("force_interactive" in arg_spec.args)
class NoninteractiveDisplayTest(unittest.TestCase):
"""Test non-interactive display.
@@ -235,6 +318,16 @@ class NoninteractiveDisplayTest(unittest.TestCase):
self.assertRaises(
errors.MissingCommandlineFlag, self.displayer.directory_select, "msg")
def test_methods_take_kwargs(self):
# Every IDisplay method implemented by NoninteractiveDisplay
# should take **kwargs because every method of FileDisplay must
# take force_interactive which doesn't apply to
# NoninteractiveDisplay.
for name in interfaces.IDisplay.names(): # pylint: disable=no-member
method = getattr(self.displayer, name)
# asserts method accepts arbitrary keyword arguments
self.assertFalse(inspect.getargspec(method).keywords is None)
class SeparateListInputTest(unittest.TestCase):
"""Test Module functions."""
+21 -5
View File
@@ -159,10 +159,12 @@ class ObtainCertTest(unittest.TestCase):
self.assertRaises(errors.ConfigurationError, self._call,
('certonly --webroot -d example.com -d test.com --cert-name example.com').split())
@mock.patch('certbot.cert_manager.domains_for_certname')
@mock.patch('certbot.display.ops.choose_names')
@mock.patch('certbot.cert_manager.lineage_for_certname')
@mock.patch('certbot.main._report_new_cert')
def test_find_lineage_for_domains_new_certname(self, mock_report_cert,
mock_lineage):
mock_lineage, mock_choose_names, mock_domains_for_certname):
mock_lineage.return_value = None
# no lineage with this name but we specified domains so create a new cert
@@ -172,8 +174,10 @@ class ObtainCertTest(unittest.TestCase):
self.assertTrue(mock_report_cert.call_count == 1)
# no lineage with this name and we didn't give domains
self.assertRaises(errors.ConfigurationError, self._call,
('certonly --webroot --cert-name example.com').split())
mock_choose_names.return_value = ["somename"]
mock_domains_for_certname.return_value = None
self._call(('certonly --webroot --cert-name example.com').split())
self.assertTrue(mock_choose_names.called)
class FindDomainsOrCertnameTest(unittest.TestCase):
"""Tests for certbot.main._find_domains_or_certname."""
@@ -193,6 +197,14 @@ class FindDomainsOrCertnameTest(unittest.TestCase):
# pylint: disable=protected-access
self.assertRaises(errors.Error, main._find_domains_or_certname, mock_config, None)
@mock.patch('certbot.cert_manager.domains_for_certname')
def test_grab_domains(self, mock_domains):
mock_config = mock.Mock(domains=None, certname="one.com")
mock_domains.return_value = ["one.com", "two.com"]
# pylint: disable=protected-access
self.assertEqual(main._find_domains_or_certname(mock_config, None),
(["one.com", "two.com"], "one.com"))
class RevokeTest(unittest.TestCase):
"""Tests for certbot.main.revoke."""
@@ -567,6 +579,11 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
self._call_no_clientmock(['certificates'])
self.assertEqual(1, mock_cert_manager.call_count)
@mock.patch('certbot.cert_manager.delete')
def test_delete(self, mock_cert_manager):
self._call_no_clientmock(['delete'])
self.assertEqual(1, mock_cert_manager.call_count)
def test_plugins(self):
flags = ['--init', '--prepare', '--authenticators', '--installers']
for args in itertools.chain(
@@ -856,8 +873,7 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
rc_path = test_util.make_lineage(self, 'sample-renewal-ancient.conf')
args = mock.MagicMock(account=None, email=None, webroot_path=None)
config = configuration.NamespaceConfig(args)
lineage = storage.RenewableCert(rc_path,
configuration.RenewerConfiguration(config))
lineage = storage.RenewableCert(rc_path, config)
renewalparams = lineage.configuration["renewalparams"]
# pylint: disable=protected-access
renewal._restore_webroot_config(config, renewalparams)
+137
View File
@@ -0,0 +1,137 @@
"""Tests for ocsp.py"""
# pylint: disable=protected-access
import unittest
import mock
from certbot import errors
out = """Missing = in header key=value
ocsp: Use -help for summary.
"""
class OCSPTest(unittest.TestCase):
_multiprocess_can_split_ = True
def setUp(self):
from certbot import ocsp
with mock.patch('certbot.ocsp.Popen') as mock_popen:
with mock.patch('certbot.util.exe_exists') as mock_exists:
mock_communicate = mock.MagicMock()
mock_communicate.communicate.return_value = (None, out)
mock_popen.return_value = mock_communicate
mock_exists.return_value = True
self.checker = ocsp.RevocationChecker()
def tearDown(self):
pass
@mock.patch('certbot.ocsp.logging.info')
@mock.patch('certbot.ocsp.Popen')
@mock.patch('certbot.util.exe_exists')
def test_init(self, mock_exists, mock_popen, mock_log):
mock_communicate = mock.MagicMock()
mock_communicate.communicate.return_value = (None, out)
mock_popen.return_value = mock_communicate
mock_exists.return_value = True
from certbot import ocsp
checker = ocsp.RevocationChecker()
self.assertEqual(mock_popen.call_count, 1)
self.assertEqual(checker.host_args("x"), ["Host=x"])
mock_communicate.communicate.return_value = (None, out.partition("\n")[2])
checker = ocsp.RevocationChecker()
self.assertEqual(checker.host_args("x"), ["Host", "x"])
self.assertEqual(checker.broken, False)
mock_exists.return_value = False
mock_popen.call_count = 0
checker = ocsp.RevocationChecker()
self.assertEqual(mock_popen.call_count, 0)
self.assertEqual(mock_log.call_count, 1)
self.assertEqual(checker.broken, True)
@mock.patch('certbot.ocsp.RevocationChecker.determine_ocsp_server')
@mock.patch('certbot.util.run_script')
def test_ocsp_revoked(self, mock_run, mock_determine):
self.checker.broken = True
mock_determine.return_value = ("", "")
self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
self.checker.broken = False
mock_run.return_value = tuple(openssl_happy[1:])
self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
self.assertEqual(mock_run.call_count, 0)
mock_determine.return_value = ("http://x.co", "x.co")
self.assertEqual(self.checker.ocsp_revoked("blah.pem", "chain.pem"), False)
mock_run.side_effect = errors.SubprocessError("Unable to load certificate launcher")
self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
self.assertEqual(mock_run.call_count, 2)
@mock.patch('certbot.ocsp.logger.debug')
@mock.patch('certbot.ocsp.logger.info')
@mock.patch('certbot.util.run_script')
def test_determine_ocsp_server(self, mock_run, mock_info, mock_debug):
uri = "http://ocsp.stg-int-x1.letsencrypt.org/"
host = "ocsp.stg-int-x1.letsencrypt.org"
mock_run.return_value = uri, ""
self.assertEqual(self.checker.determine_ocsp_server("beep"), (uri, host))
mock_run.return_value = "ftp:/" + host + "/", ""
self.assertEqual(self.checker.determine_ocsp_server("beep"), (None, None))
self.assertEqual(mock_info.call_count, 1)
c = "confusion"
mock_run.side_effect = errors.SubprocessError(c)
self.assertEqual(self.checker.determine_ocsp_server("beep"), (None, None))
self.assertTrue(c in repr(mock_debug.call_args[0][1]))
@mock.patch('certbot.ocsp.logger')
@mock.patch('certbot.util.run_script')
def test_translate_ocsp(self, mock_run, mock_log):
# pylint: disable=protected-access,star-args
mock_run.return_value = openssl_confused
from certbot import ocsp
self.assertEqual(ocsp._translate_ocsp_query(*openssl_happy), False)
self.assertEqual(ocsp._translate_ocsp_query(*openssl_confused), False)
self.assertEqual(mock_log.debug.call_count, 1)
self.assertEqual(mock_log.warn.call_count, 0)
self.assertEqual(ocsp._translate_ocsp_query(*openssl_broken), False)
self.assertEqual(mock_log.warn.call_count, 1)
self.assertEqual(ocsp._translate_ocsp_query(*openssl_revoked), True)
# pylint: disable=line-too-long
openssl_confused = ("", """
/etc/letsencrypt/live/example.org/cert.pem: good
This Update: Dec 17 00:00:00 2016 GMT
Next Update: Dec 24 00:00:00 2016 GMT
""",
"""
Response Verify Failure
139903674214048:error:27069065:OCSP routines:OCSP_basic_verify:certificate verify error:ocsp_vfy.c:138:Verify error:unable to get local issuer certificate
""")
openssl_happy = ("blah.pem", """
blah.pem: good
This Update: Dec 20 18:00:00 2016 GMT
Next Update: Dec 27 18:00:00 2016 GMT
""",
"Response verify OK")
openssl_revoked = ("blah.pem", """
blah.pem: revoked
This Update: Dec 20 01:00:00 2016 GMT
Next Update: Dec 27 01:00:00 2016 GMT
Revocation Time: Dec 20 01:46:34 2016 GMT
""",
"""Response verify OK""")
openssl_broken = ("", "tentacles", "Response verify OK")
if __name__ == '__main__':
unittest.main() # pragma: no cover
+1 -2
View File
@@ -21,8 +21,7 @@ class RenewalTest(unittest.TestCase):
rc_path = util.make_lineage(self, 'sample-renewal-ancient.conf')
args = mock.MagicMock(account=None, email=None, webroot_path=None)
config = configuration.NamespaceConfig(args)
lineage = storage.RenewableCert(
rc_path, configuration.RenewerConfiguration(config))
lineage = storage.RenewableCert(rc_path, config)
renewalparams = lineage.configuration["renewalparams"]
# pylint: disable=protected-access
from certbot import renewal
+100 -4
View File
@@ -49,7 +49,7 @@ class BaseRenewableCertTest(unittest.TestCase):
from certbot import storage
self.tempdir = tempfile.mkdtemp()
self.cli_config = configuration.RenewerConfiguration(
self.cli_config = configuration.NamespaceConfig(
namespace=mock.MagicMock(
config_dir=self.tempdir,
work_dir=self.tempdir,
@@ -57,16 +57,22 @@ class BaseRenewableCertTest(unittest.TestCase):
)
)
# TODO: maybe provide RenewerConfiguration.make_dirs?
# TODO: maybe provide NamespaceConfig.make_dirs?
# TODO: main() should create those dirs, c.f. #902
os.makedirs(os.path.join(self.tempdir, "live", "example.org"))
os.makedirs(os.path.join(self.tempdir, "archive", "example.org"))
archive_path = os.path.join(self.tempdir, "archive", "example.org")
os.makedirs(archive_path)
os.makedirs(os.path.join(self.tempdir, "renewal"))
config = configobj.ConfigObj()
for kind in ALL_FOUR:
config[kind] = os.path.join(self.tempdir, "live", "example.org",
kind_path = os.path.join(self.tempdir, "live", "example.org",
kind + ".pem")
config[kind] = kind_path
with open(os.path.join(self.tempdir, "live", "example.org",
"README"), 'a'):
pass
config["archive"] = archive_path
config.filename = os.path.join(self.tempdir, "renewal",
"example.org.conf")
config.write()
@@ -770,5 +776,95 @@ class RenewableCertTests(BaseRenewableCertTest):
storage.RenewableCert(self.config.filename, self.cli_config,
update_symlinks=True)
class DeleteFilesTest(BaseRenewableCertTest):
"""Tests for certbot.storage.delete_files"""
def setUp(self):
super(DeleteFilesTest, self).setUp()
for kind in ALL_FOUR:
kind_path = os.path.join(self.tempdir, "live", "example.org",
kind + ".pem")
with open(kind_path, 'a'):
pass
self.config.write()
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "example.org.conf")))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.assertTrue(os.path.exists(os.path.join(
self.tempdir, "archive", "example.org")))
def _call(self):
from certbot import storage
with mock.patch("certbot.storage.logger"):
storage.delete_files(self.cli_config, "example.org")
def test_delete_all_files(self):
self._call()
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "example.org.conf")))
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.assertFalse(os.path.exists(os.path.join(
self.tempdir, "archive", "example.org")))
def test_bad_renewal_config(self):
with open(self.config.filename, 'a') as config_file:
config_file.write("asdfasfasdfasdf")
self.assertRaises(errors.CertStorageError, self._call)
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "example.org.conf")))
def test_no_renewal_config(self):
os.remove(self.config.filename)
self.assertRaises(errors.CertStorageError, self._call)
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.assertFalse(os.path.exists(self.config.filename))
def test_no_cert_file(self):
os.remove(os.path.join(
self.cli_config.live_dir, "example.org", "cert.pem"))
self._call()
self.assertFalse(os.path.exists(self.config.filename))
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.assertFalse(os.path.exists(os.path.join(
self.tempdir, "archive", "example.org")))
def test_no_readme_file(self):
os.remove(os.path.join(
self.cli_config.live_dir, "example.org", "README"))
self._call()
self.assertFalse(os.path.exists(self.config.filename))
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.assertFalse(os.path.exists(os.path.join(
self.tempdir, "archive", "example.org")))
def test_livedir_not_empty(self):
with open(os.path.join(
self.cli_config.live_dir, "example.org", "other_file"), 'a'):
pass
self._call()
self.assertFalse(os.path.exists(self.config.filename))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.assertFalse(os.path.exists(os.path.join(
self.tempdir, "archive", "example.org")))
def test_no_archive(self):
archive_dir = os.path.join(self.tempdir, "archive", "example.org")
os.rmdir(archive_dir)
self._call()
self.assertFalse(os.path.exists(self.config.filename))
self.assertFalse(os.path.exists(os.path.join(
self.cli_config.live_dir, "example.org")))
self.assertFalse(os.path.exists(archive_dir))
if __name__ == "__main__":
unittest.main() # pragma: no cover
+6 -4
View File
@@ -38,20 +38,22 @@ ANSI_SGR_RED = "\033[31m"
ANSI_SGR_RESET = "\033[0m"
def run_script(params):
def run_script(params, log=logger.error):
"""Run the script with the given params.
:param list params: List of parameters to pass to Popen
:param logging.Logger log: Logger to use for errors
"""
try:
proc = subprocess.Popen(params,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stderr=subprocess.PIPE,
universal_newlines=True)
except (OSError, ValueError):
msg = "Unable to run the command: %s" % " ".join(params)
logger.error(msg)
log(msg)
raise errors.SubprocessError(msg)
stdout, stderr = proc.communicate()
@@ -60,7 +62,7 @@ def run_script(params):
msg = "Error while running %s.\n%s\n%s" % (
" ".join(params), stdout, stderr)
# Enter recovery routine...
logger.error(msg)
log(msg)
raise errors.SubprocessError(msg)
return stdout, stderr
+1 -1
View File
@@ -178,7 +178,7 @@ renew:
The 'renew' subcommand will attempt to renew all certificates (or more
precisely, certificate lineages) you have previously obtained if they are
close to expiry, and print a summary of the results. By default, 'renew'
will reuse the options used to create obtain or most recently successfully
will reuse the options used to create, obtain or most recently successfully
renew each certificate lineage. You can try it with `--dry-run` first. For
more fine-grained control, you can renew individual lineages with the
`certonly` subcommand. Hooks are available to run commands before and
+2 -3
View File
@@ -246,9 +246,8 @@ configuration checkpoints and rollback.
Display
~~~~~~~
We currently offer a pythondialog and "text" mode for displays. Display
plugins implement the `~certbot.interfaces.IDisplay`
interface.
We currently only offer a "text" mode for displays. Display plugins
implement the `~certbot.interfaces.IDisplay` interface.
.. _dev-plugin:
+3 -3
View File
@@ -61,7 +61,7 @@ manual_ Y N | Helps you obtain a cert by giving you instructions to pe
Under the hood, plugins use one of several ACME protocol "Challenges_" to
prove you control a domain. The options are http-01_ (which uses port 80),
tls-sni-01_ (port 443) and dns-01_ (requring configuration of a DNS server on
port 53, thought that's often not the same machine as your webserver). A few
port 53, though that's often not the same machine as your webserver). A few
plugins support more than one challenge type, in which case you can choose one
with ``--preferred-challenges``.
@@ -193,7 +193,7 @@ postfix_ N Y STARTTLS Everywhere is becoming a Certbot Postfix/Exim plu
=========== ==== ==== ===============================================================
.. _plesk: https://github.com/plesk/letsencrypt-plesk
.. _haproxy: https://code.greenhost.net/open/letsencrypt-haproxy
.. _haproxy: https://github.com/greenhost/certbot-haproxy
.. _s3front: https://github.com/dlapiduz/letsencrypt-s3front
.. _gandi: https://github.com/Gandi/letsencrypt-gandi
.. _icecast: https://github.com/e00E/lets-encrypt-icecast
@@ -233,7 +233,7 @@ certificate that contains all of the old domains and one or more additional
new domains.
``--allow-subset-of-names`` tells Certbot to continue with cert generation if
only some of the specified domain authorazations can be obtained. This may
only some of the specified domain authorizations can be obtained. This may
be useful if some domains specified in a certificate no longer point at this
system.
+49 -10
View File
@@ -15,9 +15,13 @@ set -e # Work even if somebody does "sh thisscript.sh".
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
# if you want to change where the virtual environment will be installed
XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
if [ -z "$XDG_DATA_HOME" ]; then
XDG_DATA_HOME="~/.local/share"
fi
VENV_NAME="letsencrypt"
VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
if [ -z "$VENV_PATH" ]; then
VENV_PATH="$XDG_DATA_HOME/$VENV_NAME"
fi
VENV_BIN="$VENV_PATH/bin"
LE_AUTO_VERSION="0.10.0.dev0"
BASENAME=$(basename $0)
@@ -80,6 +84,17 @@ if [ $BASENAME = "letsencrypt-auto" ]; then
HELP=0
fi
# Support for busybox and others where there is no "command",
# but "which" instead
if command -v command > /dev/null 2>&1 ; then
export EXISTS="command -v"
elif which which > /dev/null 2>&1 ; then
export EXISTS="which"
else
echo "Cannot find command nor which... please install one!"
exit 1
fi
# certbot-auto needs root access to bootstrap OS dependencies, and
# certbot itself needs root access for almost all modes of operation
# The "normal" case is that sudo is used for the steps that need root, but
@@ -127,7 +142,7 @@ if [ -n "${LE_AUTO_SUDO+x}" ]; then
echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
else
if test "`id -u`" -ne "0" ; then
if command -v sudo 1>/dev/null 2>&1; then
if $EXISTS sudo 1>/dev/null 2>&1; then
SUDO=sudo
SUDO_ENV="CERTBOT_AUTO=$0"
else
@@ -157,7 +172,7 @@ ExperimentalBootstrap() {
DeterminePythonVersion() {
for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
# Break (while keeping the LE_PYTHON value) if found.
command -v "$LE_PYTHON" > /dev/null && break
$EXISTS "$LE_PYTHON" > /dev/null && break
done
if [ "$?" != "0" ]; then
echo "Cannot find any Pythons; please install one!"
@@ -198,19 +213,22 @@ BootstrapDebCommon() {
# distro version (#346)
virtualenv=
if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
virtualenv="virtualenv"
# virtual env is known to apt and is installable
if apt-cache show virtualenv > /dev/null 2>&1 ; then
if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
virtualenv="virtualenv"
fi
fi
if apt-cache show python-virtualenv > /dev/null 2>&1; then
virtualenv="$virtualenv python-virtualenv"
virtualenv="$virtualenv python-virtualenv"
fi
augeas_pkg="libaugeas0 augeas-lenses"
AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
if [ "$ASSUME_YES" = 1 ]; then
YES_FLAG="-y"
YES_FLAG="-y"
fi
AddBackportRepo() {
@@ -276,7 +294,7 @@ BootstrapDebCommon() {
if ! command -v virtualenv > /dev/null ; then
if ! $EXISTS virtualenv > /dev/null ; then
echo Failed to install a working \"virtualenv\" command, exiting
exit 1
fi
@@ -960,7 +978,28 @@ UNLIKELY_EOF
# Report error. (Otherwise, be quiet.)
echo "Had a problem while installing Python packages."
if [ "$VERBOSE" != 1 ]; then
echo
echo "pip prints the following errors: "
echo "====================================================="
echo "$PIP_OUT"
echo "====================================================="
echo
echo "Certbot has problem setting up the virtual environment."
if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then
echo
echo "Based on your pip output, the problem can likely be fixed by "
echo "increasing the available memory."
else
echo
echo "We were not be able to guess the right solution from your pip "
echo "output."
fi
echo
echo "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment"
echo "for possible solutions."
echo "You may also find some support resources at https://certbot.eff.org/support/ ."
fi
rm -rf "$VENV_PATH"
exit 1
@@ -23,19 +23,22 @@ BootstrapDebCommon() {
# distro version (#346)
virtualenv=
if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
virtualenv="virtualenv"
# virtual env is known to apt and is installable
if apt-cache show virtualenv > /dev/null 2>&1 ; then
if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
virtualenv="virtualenv"
fi
fi
if apt-cache show python-virtualenv > /dev/null 2>&1; then
virtualenv="$virtualenv python-virtualenv"
virtualenv="$virtualenv python-virtualenv"
fi
augeas_pkg="libaugeas0 augeas-lenses"
AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
if [ "$ASSUME_YES" = 1 ]; then
YES_FLAG="-y"
YES_FLAG="-y"
fi
AddBackportRepo() {
-1
View File
@@ -131,7 +131,6 @@ setup(
'null = certbot.plugins.null:Installer',
'standalone = certbot.plugins.standalone:Authenticator',
'webroot = certbot.plugins.webroot:Authenticator',
'script = certbot.plugins.script:Authenticator',
],
},
)
+24 -19
View File
@@ -33,6 +33,23 @@ common() {
"$@"
}
CheckHooks() {
EXPECTED="/tmp/expected$$"
echo "wtf.pre" > "$EXPECTED"
echo "wtf2.pre" >> "$EXPECTED"
echo "renew" >> "$EXPECTED"
echo "renew" >> "$EXPECTED"
echo "wtf.post" > "$EXPECTED"
echo "wtf2.post" >> "$EXPECTED"
if cmp --quiet "$EXPECTED" "$HOOK_TEST" ; then
echo Hooks did not run as expected\; got
cat "$HOOK_TEST"
echo Expected
cat "$EXPECTED"
fi
[ -f "$HOOK_TEST" ] && rm -f "$HOOK_TEST"
}
# We start a server listening on the port for the
# unrequested challenge to prevent regressions in #3601.
python -m SimpleHTTPServer $http_01_port &
@@ -52,26 +69,14 @@ common --domains le2.wtf --preferred-challenges http-01 run \
--renew-hook 'echo renew >> "$HOOK_TEST"'
kill $python_server_pid
common -a manual -d le.wtf auth --rsa-key-size 4096 \
--pre-hook 'echo wtf2.pre >> "$HOOK_TEST"' \
--post-hook 'echo wtf2.post >> "$HOOK_TEST"'
common certonly -a manual -d le.wtf --rsa-key-size 4096 \
--manual-auth-hook ./tests/manual-http-auth.sh \
--manual-cleanup-hook ./tests/manual-http-cleanup.sh
--pre-hook 'echo wtf2.pre >> "$HOOK_TEST"' \
--post-hook 'echo wtf2.post >> "$HOOK_TEST"'
CheckHooks() {
EXPECTED="/tmp/expected$$"
echo "wtf.pre" > "$EXPECTED"
echo "wtf2.pre" >> "$EXPECTED"
echo "renew" >> "$EXPECTED"
echo "renew" >> "$EXPECTED"
echo "wtf.post" > "$EXPECTED"
echo "wtf2.post" >> "$EXPECTED"
if cmp --quiet "$EXPECTED" "$HOOK_TEST" ; then
echo Hooks did not run as expected\; got
cat "$HOOK_TEST"
echo Expected
cat "$EXPECTED"
fi
[ -f "$HOOK_TEST" ] && rm -f "$HOOK_TEST"
}
common certonly -a manual -d dns.le.wtf --preferred-challenges dns-01 \
--manual-auth-hook ./tests/manual-dns-auth.sh
export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
OPENSSL_CNF=examples/openssl.cnf
+1 -1
View File
@@ -18,5 +18,5 @@ def test_visual(displayer, choices):
if __name__ == "__main__":
displayer = util.FileDisplay(sys.stdout):
displayer = util.FileDisplay(sys.stdout, False)
test_visual(displayer, util_test.CHOICES)
+1 -1
View File
@@ -25,7 +25,7 @@ certbot_test_no_force_renew () {
--no-verify-ssl \
--tls-sni-01-port $tls_sni_01_port \
--http-01-port $http_01_port \
--manual-test-mode \
--manual-public-ip-logging-ok \
$store_flags \
--non-interactive \
--no-redirect \
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
curl -X POST 'http://localhost:8055/set-txt' -d \
"{\"host\": \"_acme-challenge.$CERTBOT_DOMAIN.\", \
\"value\": \"$CERTBOT_VALIDATION\"}"
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
uri_path=".well-known/acme-challenge/$CERTBOT_TOKEN"
cd $(mktemp -d)
mkdir -p $(dirname $uri_path)
echo $CERTBOT_VALIDATION > $uri_path
python -m SimpleHTTPServer $http_01_port >/dev/null 2>&1 &
server_pid=$!
while ! curl "http://localhost:$http_01_port/$uri_path" >/dev/null 2>&1; do
sleep 1s
done
echo $server_pid
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
kill $CERTBOT_AUTH_OUTPUT
+2 -3
View File
@@ -36,9 +36,8 @@ cover () {
# specific package, positional argument scopes tests only to
# specific package directory; --cover-tests makes sure every tests
# is run (c.f. #403)
nosetests -c /dev/null --with-cover --cover-tests --cover-package \
"$1" --cover-min-percentage="$min" "$1" --processes=-1 \
--process-timeout=100
nosetests -c /dev/null --with-cover --cover-tests --cover-package \
"$1" --cover-min-percentage="$min" "$1"
}
rm -f .coverage # --cover-erase is off, make sure stats are correct
+5
View File
@@ -63,6 +63,11 @@ commands =
pip install -e .[dev]
nosetests -v certbot --processes=-1 --process-timeout=100
[testenv:py27_install]
basepython = python2.7
commands =
pip install -e acme[dns,dev] -e .[dev] -e certbot-apache -e certbot-nginx -e letshelp-certbot
[testenv:cover]
basepython = python2.7
commands =