mirror of
https://github.com/certbot/certbot.git
synced 2026-06-08 16:22:18 -04:00
Merge remote-tracking branch 'github/letsencrypt/master' into cli-config-fixes
Conflicts: letsencrypt_apache/constants.py letsencrypt_nginx/constants.py
This commit is contained in:
commit
196884652e
17 changed files with 180 additions and 52 deletions
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -1,13 +1,18 @@
|
|||
*.pyc
|
||||
*.egg-info
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
||||
venv/
|
||||
.tox/
|
||||
/venv/
|
||||
/.tox/
|
||||
|
||||
# coverage
|
||||
.coverage
|
||||
m3
|
||||
/htmlcov/
|
||||
|
||||
/.vagrant
|
||||
|
||||
# editor temporary files
|
||||
*~
|
||||
.vagrant
|
||||
*.swp
|
||||
\#*#
|
||||
|
|
@ -9,7 +9,7 @@ recursive-include acme/schemata *.json
|
|||
recursive-include acme/jose/testdata *
|
||||
|
||||
recursive-include letsencrypt_apache/tests/testdata *
|
||||
include letsencrypt_apache/options-ssl.conf
|
||||
include letsencrypt_apache/options-ssl-apache.conf
|
||||
|
||||
recursive-include letsencrypt_nginx/tests/testdata *
|
||||
include letsencrypt_nginx/options-ssl.conf
|
||||
include letsencrypt_nginx/options-ssl-nginx.conf
|
||||
|
|
|
|||
|
|
@ -396,7 +396,7 @@ def main(args=sys.argv[1:]):
|
|||
# Reporter
|
||||
report = reporter.Reporter()
|
||||
zope.component.provideUtility(report)
|
||||
atexit.register(report.print_messages)
|
||||
atexit.register(report.atexit_print_messages)
|
||||
|
||||
# Logging
|
||||
level = -args.verbose_count * 10
|
||||
|
|
|
|||
|
|
@ -99,6 +99,29 @@ class Client(object):
|
|||
raise errors.LetsEncryptClientError("Must agree to TOS")
|
||||
|
||||
self.account.save()
|
||||
self._report_new_account()
|
||||
|
||||
def _report_new_account(self):
|
||||
"""Informs the user about their new Let's Encrypt account."""
|
||||
reporter = zope.component.getUtility(interfaces.IReporter)
|
||||
reporter.add_message(
|
||||
"Your account credentials have been saved in your Let's Encrypt "
|
||||
"configuration directory at {0}. You should make a secure backup "
|
||||
"of this folder now. This configuration directory will also "
|
||||
"contain certificates and private keys obtained by Let's Encrypt "
|
||||
"so making regular backups of this folder is ideal.".format(
|
||||
self.config.config_dir),
|
||||
reporter.MEDIUM_PRIORITY, True)
|
||||
|
||||
assert self.account.recovery_token is not None
|
||||
recovery_msg = ("If you lose your account credentials, you can recover "
|
||||
"them using the token \"{0}\". You must write that down "
|
||||
"and put it in a safe place.".format(
|
||||
self.account.recovery_token))
|
||||
if self.account.email is not None:
|
||||
recovery_msg += (" Another recovery method will be e-mails sent to "
|
||||
"{0}.".format(self.account.email))
|
||||
reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True)
|
||||
|
||||
def obtain_certificate(self, domains, csr=None):
|
||||
"""Obtains a certificate from the ACME server.
|
||||
|
|
@ -195,9 +218,38 @@ class Client(object):
|
|||
params = vars(self.config.namespace)
|
||||
config = {"renewer_config_file":
|
||||
params["renewer_config_file"]} if "renewer_config_file" in params else None
|
||||
return storage.RenewableCert.new_lineage(domains[0], cert, privkey,
|
||||
chain, params, config)
|
||||
renewable_cert = storage.RenewableCert.new_lineage(domains[0], cert, privkey,
|
||||
chain, params, config)
|
||||
self._report_renewal_status(renewable_cert)
|
||||
return renewable_cert
|
||||
|
||||
def _report_renewal_status(self, cert):
|
||||
# pylint: disable=no-self-use
|
||||
"""Informs the user about automatic renewal and deployment.
|
||||
|
||||
:param cert: Newly issued certificate
|
||||
:type cert: :class:`letsencrypt.storage.RenewableCert`
|
||||
|
||||
"""
|
||||
if ("autorenew" not in cert.configuration
|
||||
or cert.configuration.as_bool("autorenew")):
|
||||
if ("autodeploy" not in cert.configuration or
|
||||
cert.configuration.as_bool("autodeploy")):
|
||||
msg = "Automatic renewal and deployment has "
|
||||
else:
|
||||
msg = "Automatic renewal but not automatic deployment has "
|
||||
else:
|
||||
if ("autodeploy" not in cert.configuration or
|
||||
cert.configuration.as_bool("autodeploy")):
|
||||
msg = "Automatic deployment but not automatic renewal has "
|
||||
else:
|
||||
msg = "Automatic renewal and deployment has not "
|
||||
|
||||
msg += ("been enabled for your certificate. These settings can be "
|
||||
"configured in the directories under {0}.").format(
|
||||
cert.configuration["renewal_configs_dir"])
|
||||
reporter = zope.component.getUtility(interfaces.IReporter)
|
||||
reporter.add_message(msg, reporter.LOW_PRIORITY, True)
|
||||
|
||||
def save_certificate(self, certr, cert_path, chain_path):
|
||||
# pylint: disable=no-self-use
|
||||
|
|
|
|||
|
|
@ -54,8 +54,25 @@ def check_permissions(filepath, mode, uid=0):
|
|||
return stat.S_IMODE(file_stat.st_mode) == mode and file_stat.st_uid == uid
|
||||
|
||||
|
||||
def _safely_attempt_open(fname, mode):
|
||||
file_d = os.open(fname, os.O_CREAT | os.O_EXCL | os.O_RDWR, mode)
|
||||
return os.fdopen(file_d, "w"), fname
|
||||
|
||||
|
||||
def _unique_file(path, filename_pat, count, mode):
|
||||
while True:
|
||||
try:
|
||||
return _safely_attempt_open(
|
||||
os.path.join(path, filename_pat(count)), mode)
|
||||
except OSError as err:
|
||||
# "File exists," is okay, try a different name.
|
||||
if err.errno != errno.EEXIST:
|
||||
raise
|
||||
count += 1
|
||||
|
||||
|
||||
def unique_file(path, mode=0o777):
|
||||
"""Safely finds a unique file for writing only (by default).
|
||||
"""Safely finds a unique file.
|
||||
|
||||
:param str path: path/filename.ext
|
||||
:param int mode: File mode
|
||||
|
|
@ -64,52 +81,35 @@ def unique_file(path, mode=0o777):
|
|||
|
||||
"""
|
||||
path, tail = os.path.split(path)
|
||||
count = 0
|
||||
while True:
|
||||
fname = os.path.join(path, "%04d_%s" % (count, tail))
|
||||
try:
|
||||
file_d = os.open(fname, os.O_CREAT | os.O_EXCL | os.O_RDWR, mode)
|
||||
return os.fdopen(file_d, "w"), fname
|
||||
except OSError as exception:
|
||||
# "File exists," is okay, try a different name.
|
||||
if exception.errno != errno.EEXIST:
|
||||
raise
|
||||
count += 1
|
||||
return _unique_file(
|
||||
path, filename_pat=(lambda count: "%04d_%s" % (count, tail)),
|
||||
count=0, mode=mode)
|
||||
|
||||
|
||||
def unique_lineage_name(path, filename, mode=0o777):
|
||||
"""Safely finds a unique file for writing only (by default). Uses a
|
||||
file lineage convention.
|
||||
"""Safely finds a unique file using lineage convention.
|
||||
|
||||
:param str path: directory path
|
||||
:param str filename: proposed filename
|
||||
:param int mode: file mode
|
||||
|
||||
:returns: tuple of file object and file name (which may be modified from
|
||||
the requested one by appending digits to ensure uniqueness)
|
||||
:returns: tuple of file object and file name (which may be modified
|
||||
from the requested one by appending digits to ensure uniqueness)
|
||||
|
||||
:raises OSError: if writing files fails for an unanticipated reason,
|
||||
such as a full disk or a lack of permission to write to specified
|
||||
location.
|
||||
such as a full disk or a lack of permission to write to
|
||||
specified location.
|
||||
|
||||
"""
|
||||
fname = os.path.join(path, "%s.conf" % (filename))
|
||||
try:
|
||||
file_d = os.open(fname, os.O_CREAT | os.O_EXCL | os.O_RDWR, mode)
|
||||
return os.fdopen(file_d, "w"), fname
|
||||
return _safely_attempt_open(
|
||||
os.path.join(path, "%s.conf" % (filename)), mode=mode)
|
||||
except OSError as err:
|
||||
if err.errno != errno.EEXIST:
|
||||
raise err
|
||||
count = 1
|
||||
while True:
|
||||
fname = os.path.join(path, "%s-%04d.conf" % (filename, count))
|
||||
try:
|
||||
file_d = os.open(fname, os.O_CREAT | os.O_EXCL | os.O_RDWR, mode)
|
||||
return os.fdopen(file_d, "w"), fname
|
||||
except OSError as err:
|
||||
if err.errno != errno.EEXIST:
|
||||
raise err
|
||||
count += 1
|
||||
raise
|
||||
return _unique_file(
|
||||
path, filename_pat=(lambda count: "%s-%04d.conf" % (filename, count)),
|
||||
count=1, mode=mode)
|
||||
|
||||
|
||||
def safely_remove(path):
|
||||
|
|
|
|||
|
|
@ -152,7 +152,6 @@ class StandaloneAuthenticator(common.Plugin):
|
|||
:rtype: bool
|
||||
|
||||
"""
|
||||
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
|
||||
start_time = time.time()
|
||||
|
|
@ -266,6 +265,7 @@ class StandaloneAuthenticator(common.Plugin):
|
|||
signal.signal(signal.SIGUSR1, self.client_signal_handler)
|
||||
signal.signal(signal.SIGUSR2, self.client_signal_handler)
|
||||
|
||||
sys.stdout.flush()
|
||||
fork_result = os.fork()
|
||||
Crypto.Random.atfork()
|
||||
if fork_result:
|
||||
|
|
|
|||
|
|
@ -317,6 +317,7 @@ class PerformTest(unittest.TestCase):
|
|||
"""What happens if start_listener() returns True."""
|
||||
self.authenticator.start_listener = mock.Mock()
|
||||
self.authenticator.start_listener.return_value = True
|
||||
self.authenticator.already_listening = mock.Mock(return_value=False)
|
||||
result = self.authenticator.perform(self.achalls)
|
||||
self.assertEqual(len(self.authenticator.tasks), 2)
|
||||
self.assertTrue(
|
||||
|
|
@ -335,6 +336,7 @@ class PerformTest(unittest.TestCase):
|
|||
"""What happens if start_listener() returns False."""
|
||||
self.authenticator.start_listener = mock.Mock()
|
||||
self.authenticator.start_listener.return_value = False
|
||||
self.authenticator.already_listening = mock.Mock(return_value=False)
|
||||
result = self.authenticator.perform(self.achalls)
|
||||
self.assertEqual(len(self.authenticator.tasks), 2)
|
||||
self.assertTrue(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"""Collects and displays information to the user."""
|
||||
import collections
|
||||
import logging
|
||||
import os
|
||||
import Queue
|
||||
import sys
|
||||
import textwrap
|
||||
|
|
@ -46,20 +48,31 @@ class Reporter(object):
|
|||
"""
|
||||
assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY
|
||||
self.messages.put(self._msg_type(priority, msg, on_crash))
|
||||
logging.info("Reporting to user: %s", msg)
|
||||
|
||||
def atexit_print_messages(self, pid=os.getpid()):
|
||||
"""Function to be registered with atexit to print messages.
|
||||
|
||||
:param int pid: Process ID
|
||||
|
||||
"""
|
||||
# This ensures that messages are only printed from the process that
|
||||
# created the Reporter.
|
||||
if pid == os.getpid():
|
||||
self.print_messages()
|
||||
|
||||
def print_messages(self):
|
||||
"""Prints messages to the user and clears the message queue.
|
||||
|
||||
If there is an unhandled exception, only messages for which
|
||||
``on_crash`` is ``True`` are printed.
|
||||
|
||||
"""
|
||||
"""
|
||||
bold_on = False
|
||||
if not self.messages.empty():
|
||||
no_exception = sys.exc_info()[0] is None
|
||||
bold_on = sys.stdout.isatty()
|
||||
if bold_on:
|
||||
sys.stdout.write(self._BOLD)
|
||||
print self._BOLD
|
||||
print 'IMPORTANT NOTES:'
|
||||
wrapper = textwrap.TextWrapper(initial_indent=' - ',
|
||||
subsequent_indent=(' ' * 3))
|
||||
|
|
|
|||
|
|
@ -125,10 +125,9 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
|||
if combos:
|
||||
authz_kwargs.update({"combinations": gen_combos(challbs)})
|
||||
if authz_status == messages2.STATUS_VALID:
|
||||
now = datetime.datetime.now()
|
||||
authz_kwargs.update({
|
||||
"status": authz_status,
|
||||
"expires": datetime.datetime(now.year, now.month + 1, now.day),
|
||||
"expires": datetime.datetime.now() + datetime.timedelta(days=31),
|
||||
})
|
||||
else:
|
||||
authz_kwargs.update({
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import pkg_resources
|
|||
import shutil
|
||||
import tempfile
|
||||
|
||||
import configobj
|
||||
import mock
|
||||
|
||||
from letsencrypt import account
|
||||
|
|
@ -35,6 +36,50 @@ class ClientTest(unittest.TestCase):
|
|||
self.network2.Network.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, verify_ssl=True)
|
||||
|
||||
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||
def test_report_new_account(self, mock_zope):
|
||||
# pylint: disable=protected-access
|
||||
self.config.config_dir = "/usr/bin/coffee"
|
||||
self.account.recovery_token = "ECCENTRIC INVISIBILITY RHINOCEROS"
|
||||
self.account.email = "rhino@jungle.io"
|
||||
|
||||
self.client._report_new_account()
|
||||
call_list = mock_zope().add_message.call_args_list
|
||||
self.assertTrue(self.config.config_dir in call_list[0][0][0])
|
||||
self.assertTrue(self.account.recovery_token in call_list[1][0][0])
|
||||
self.assertTrue(self.account.email in call_list[1][0][0])
|
||||
|
||||
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||
def test_report_renewal_status(self, mock_zope):
|
||||
# pylint: disable=protected-access
|
||||
cert = mock.MagicMock()
|
||||
cert.configuration = configobj.ConfigObj()
|
||||
cert.configuration["renewal_configs_dir"] = "/etc/letsencrypt/configs"
|
||||
|
||||
cert.configuration["autorenew"] = "True"
|
||||
cert.configuration["autodeploy"] = "True"
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("renewal and deployment has been" in msg)
|
||||
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
|
||||
|
||||
cert.configuration["autorenew"] = "False"
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("deployment but not automatic renewal" in msg)
|
||||
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
|
||||
|
||||
cert.configuration["autodeploy"] = "False"
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("renewal and deployment has not" in msg)
|
||||
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
|
||||
|
||||
cert.configuration["autorenew"] = "True"
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("renewal but not automatic deployment" in msg)
|
||||
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
|
||||
|
||||
class DetermineAccountTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.determine_authenticator."""
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import unittest
|
|||
|
||||
import mock
|
||||
|
||||
from letsencrypt import errors
|
||||
|
||||
|
||||
class MakeOrVerifyDirTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.le_util.make_or_verify_dir.
|
||||
|
|
@ -42,7 +44,8 @@ class MakeOrVerifyDirTest(unittest.TestCase):
|
|||
self.assertEqual(stat.S_IMODE(os.stat(self.path).st_mode), 0o400)
|
||||
|
||||
def test_existing_wrong_mode_fails(self):
|
||||
self.assertRaises(Exception, self._call, self.path, 0o600)
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptClientError, self._call, self.path, 0o600)
|
||||
|
||||
def test_reraises_os_error(self):
|
||||
with mock.patch.object(os, 'makedirs') as makedirs:
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class ProofOfPossessionTest(unittest.TestCase):
|
|||
def test_perform_no_input(self):
|
||||
self.assertTrue(self.proof_of_pos.perform(self.achall).verify())
|
||||
|
||||
@mock.patch("letsencrypt.recovery_token.zope.component.getUtility")
|
||||
@mock.patch("letsencrypt.proof_of_possession.zope.component.getUtility")
|
||||
def test_perform_with_input(self, mock_input):
|
||||
# Remove the matching certificate
|
||||
self.installer.get_all_certs_keys.return_value.pop()
|
||||
|
|
|
|||
|
|
@ -30,6 +30,15 @@ class ReporterTest(unittest.TestCase):
|
|||
self.reporter.print_messages()
|
||||
self.assertEqual(sys.stdout.getvalue(), "")
|
||||
|
||||
def test_atexit_print_messages(self):
|
||||
self._add_messages()
|
||||
self.reporter.atexit_print_messages()
|
||||
output = sys.stdout.getvalue()
|
||||
self.assertTrue("IMPORTANT NOTES:" in output)
|
||||
self.assertTrue("High" in output)
|
||||
self.assertTrue("Med" in output)
|
||||
self.assertTrue("Low" in output)
|
||||
|
||||
def test_tty_successful_exit(self):
|
||||
sys.stdout.isatty = lambda: True
|
||||
self._successful_exit_common()
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ MOD_SSL_CONF_DEST = "options-ssl-apache.conf"
|
|||
"""Name of the mod_ssl config file as saved in `IConfig.config_dir`."""
|
||||
|
||||
MOD_SSL_CONF_SRC = pkg_resources.resource_filename(
|
||||
"letsencrypt_apache", "options-ssl.conf")
|
||||
"letsencrypt_apache", "options-ssl-apache.conf")
|
||||
"""Path to the Apache mod_ssl config file found in the Let's Encrypt
|
||||
distribution."""
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@ MOD_SSL_CONF_DEST = "options-ssl-nginx.conf"
|
|||
"""Name of the mod_ssl config file as saved in `IConfig.config_dir`."""
|
||||
|
||||
MOD_SSL_CONF_SRC = pkg_resources.resource_filename(
|
||||
"letsencrypt_nginx", "options-ssl.conf")
|
||||
"letsencrypt_nginx", "options-ssl-nginx.conf")
|
||||
"""Path to the nginx mod_ssl config file found in the Let's Encrypt
|
||||
distribution."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue