mirror of
https://github.com/certbot/certbot.git
synced 2026-06-05 06:42:10 -04:00
Streamline and reorganize Certbot's CLI output.
This change is a substantial command-line UX overhaul,
based on previous user research. The main goal was to streamline
and clarify output. To see more verbose output, use the -v or -vv flags.
---
* nginx,apache: CLI logging changes
- Add "Successfully deployed ..." message using display_util
- Remove IReporter usage and replace with display_util
- Standardize "... could not find a VirtualHost ..." error
This changes also bumps the version of certbot required by certbot-nginx
and certbot-apache to take use of the new display_util function.
* fix certbot_compatibility_test
since the http plugins now require IDisplay, we need to inject it
* fix dependency version on certbot
* use better asserts
* try fix oldest deps
because certbot 1.10.0 depends on acme>=1.8.0, we need to use
acme==1.8.0 in the -oldest tests
* cli: redesign output of new certificate reporting
Changes the output of run, certonly and certonly --csr. No longer uses
IReporter.
* cli: redesign output of failed authz reporting
* fix problem sorting to be stable between py2 & 3
* add some catch-all error text
* cli: dont use IReporter for EFF donation prompt
* add per-authenticator hints
* pass achalls to auth_hint, write some tests
* exclude static auth hints from coverage
* dont call auth_hint unless derived from .Plugin
* dns fallback hint: dont assume --dns-blah works
--dns-blah won't work for third-party plugins, they need to be specified
using --authenticator dns-blah.
* add code comments about the auth_hint interface
* renew: don't restart the installer for dry-runs
Prevents Certbot from superfluously invoking the installer restart
during dry-run renewals. (This does not affect authenticator restarts).
Additionally removes some CLI output that was reporting the fullchain
path of the renewed certificate.
* update CHANGELOG.md
* cli: redesign output when cert installation failed
- Display a message when certificate installation begins.
- Don't use IReporter, just log errors immediately if restart/rollback
fails.
- Prompt the user with a command to retry the installation process once
they have fixed any underlying problems.
* vary by preconfigured_renewal
and move expiry date to be above the renewal advice
* update code comment
Co-authored-by: ohemorange <ebportnoy@gmail.com>
* update code comment
Co-authored-by: ohemorange <ebportnoy@gmail.com>
* fix lint
* derve cert name from cert_path, if possible
* fix type annotation
* text change in nginx hint
Co-authored-by: ohemorange <ebportnoy@gmail.com>
* print message when restarting server after renewal
* log: print "advice" when exiting with an error
When running in non-quiet mode.
* try fix -oldest lock_test.py
* fix docstring
* s/Restarting/Reloading/ when notifying the user
* fix test name
Co-authored-by: ohemorange <ebportnoy@gmail.com>
* type annotations
* s/using the {} plugin/installer: {}/
* copy: avoid "plugin" where possible
* link to user guide#automated-renewals
when not running with --preconfigured-renewal
* cli: reduce default logging verbosity
* fix lock_test: -vv is needed to see logger.debug
* Change comment in log.py to match the change to default verbosity
* Audit and adjust logging levels in apache module
* Audit and adjust logging levels in nginx module
* Audit, adjust logging levels, and improve logging calls in certbot module
* Fix tests to mock correct methods and classes
* typo in non-preconfigured-renewal message
Co-authored-by: ohemorange <ebportnoy@gmail.com>
* fix test
* revert acme version bump
* catch up to python3 changes
* Revert "revert acme version bump"
This reverts commit fa83d6a51c.
* Change ocsp check error to warning since it's non-fatal
* Update storage_test in parallel with last change
* get rid of leading newline on "Deploying [...]"
* shrink renewal and installation success messages
* print logfile rather than logdir in exit handler
* Decrease logging level to info for idempotent operation where enhancement is already set
* Display cert not yet due for renewal message when renewing and no other action will be taken, and change cert to certificate
* also write to logger so it goes in the log file
* Don't double write to log file; fix main test
* cli: remove trailing newline on new cert reporting
* ignore type error
* revert accidental changes to dependencies
* Pass tests in any timezone by using utcfromtimestamp
* Add changelog entry
* fix nits
* Improve wording of try again message
* minor wording change to changelog
* hooks: send hook stdout to CLI stdout
includes both --manual and --{pre,post,renew} hooks
* update docstrings and remove TODO
* add a pending deprecation on execute_command
* add test coverage for both
* update deprecation text
Co-authored-by: ohemorange <ebportnoy@gmail.com>
Co-authored-by: Alex Zorin <alex@zorin.id.au>
Co-authored-by: alexzorin <alex@zor.io>
207 lines
7.9 KiB
Python
207 lines
7.9 KiB
Python
"""A class that performs HTTP-01 challenges for Nginx"""
|
|
|
|
import io
|
|
import logging
|
|
from typing import List
|
|
from typing import Optional
|
|
|
|
from acme import challenges
|
|
from certbot import achallenges
|
|
from certbot import errors
|
|
from certbot.compat import os
|
|
from certbot.plugins import common
|
|
from certbot_nginx._internal import nginxparser
|
|
from certbot_nginx._internal import obj
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class NginxHttp01(common.ChallengePerformer):
|
|
"""HTTP-01 authenticator for Nginx
|
|
|
|
:ivar configurator: NginxConfigurator object
|
|
:type configurator: :class:`~nginx.configurator.NginxConfigurator`
|
|
|
|
:ivar list achalls: Annotated
|
|
class:`~certbot.achallenges.KeyAuthorizationAnnotatedChallenge`
|
|
challenges
|
|
|
|
:param list indices: Meant to hold indices of challenges in a
|
|
larger array. NginxHttp01 is capable of solving many challenges
|
|
at once which causes an indexing issue within NginxConfigurator
|
|
who must return all responses in order. Imagine
|
|
NginxConfigurator maintaining state about where all of the
|
|
challenges, possibly of different types, belong in the response
|
|
array. This is an optional utility.
|
|
|
|
"""
|
|
|
|
def __init__(self, configurator):
|
|
super().__init__(configurator)
|
|
self.challenge_conf = os.path.join(
|
|
configurator.config.config_dir, "le_http_01_cert_challenge.conf")
|
|
|
|
def perform(self):
|
|
"""Perform a challenge on Nginx.
|
|
|
|
:returns: list of :class:`certbot.acme.challenges.HTTP01Response`
|
|
:rtype: list
|
|
|
|
"""
|
|
if not self.achalls:
|
|
return []
|
|
|
|
responses = [x.response(x.account_key) for x in self.achalls]
|
|
|
|
# Set up the configuration
|
|
self._mod_config()
|
|
|
|
# Save reversible changes
|
|
self.configurator.save("HTTP Challenge", True)
|
|
|
|
return responses
|
|
|
|
def _mod_config(self):
|
|
"""Modifies Nginx config to include server_names_hash_bucket_size directive
|
|
and server challenge blocks.
|
|
|
|
:raises .MisconfigurationError:
|
|
Unable to find a suitable HTTP block in which to include
|
|
authenticator hosts.
|
|
"""
|
|
included = False
|
|
include_directive = ['\n', 'include', ' ', self.challenge_conf]
|
|
root = self.configurator.parser.config_root
|
|
|
|
bucket_directive = ['\n', 'server_names_hash_bucket_size', ' ', '128']
|
|
|
|
main = self.configurator.parser.parsed[root]
|
|
for line in main:
|
|
if line[0] == ['http']:
|
|
body = line[1]
|
|
found_bucket = False
|
|
posn = 0
|
|
for inner_line in body:
|
|
if inner_line[0] == bucket_directive[1]:
|
|
if int(inner_line[1]) < int(bucket_directive[3]):
|
|
body[posn] = bucket_directive
|
|
found_bucket = True
|
|
posn += 1
|
|
if not found_bucket:
|
|
body.insert(0, bucket_directive)
|
|
if include_directive not in body:
|
|
body.insert(0, include_directive)
|
|
included = True
|
|
break
|
|
if not included:
|
|
raise errors.MisconfigurationError(
|
|
'Certbot could not find a block to include '
|
|
'challenges in %s.' % root)
|
|
config = [self._make_or_mod_server_block(achall) for achall in self.achalls]
|
|
config = [x for x in config if x is not None]
|
|
config = nginxparser.UnspacedList(config)
|
|
logger.debug("Generated server block:\n%s", str(config))
|
|
|
|
self.configurator.reverter.register_file_creation(
|
|
True, self.challenge_conf)
|
|
|
|
with io.open(self.challenge_conf, "w", encoding="utf-8") as new_conf:
|
|
nginxparser.dump(config, new_conf)
|
|
|
|
def _default_listen_addresses(self):
|
|
"""Finds addresses for a challenge block to listen on.
|
|
:returns: list of :class:`certbot_nginx._internal.obj.Addr` to apply
|
|
:rtype: list
|
|
"""
|
|
addresses: List[obj.Addr] = []
|
|
default_addr = "%s" % self.configurator.config.http01_port
|
|
ipv6_addr = "[::]:{0}".format(
|
|
self.configurator.config.http01_port)
|
|
port = self.configurator.config.http01_port
|
|
|
|
ipv6, ipv6only = self.configurator.ipv6_info(port)
|
|
|
|
if ipv6:
|
|
# If IPv6 is active in Nginx configuration
|
|
if not ipv6only:
|
|
# If ipv6only=on is not already present in the config
|
|
ipv6_addr = ipv6_addr + " ipv6only=on"
|
|
addresses = [obj.Addr.fromstring(default_addr),
|
|
obj.Addr.fromstring(ipv6_addr)]
|
|
logger.debug(("Using default addresses %s and %s for authentication."),
|
|
default_addr,
|
|
ipv6_addr)
|
|
else:
|
|
addresses = [obj.Addr.fromstring(default_addr)]
|
|
logger.debug("Using default address %s for authentication.",
|
|
default_addr)
|
|
return addresses
|
|
|
|
def _get_validation_path(self, achall):
|
|
return os.sep + os.path.join(challenges.HTTP01.URI_ROOT_PATH, achall.chall.encode("token"))
|
|
|
|
def _make_server_block(self, achall: achallenges.KeyAuthorizationAnnotatedChallenge) -> List:
|
|
"""Creates a server block for a challenge.
|
|
|
|
:param achall: Annotated HTTP-01 challenge
|
|
:type achall: :class:`certbot.achallenges.KeyAuthorizationAnnotatedChallenge`
|
|
|
|
:returns: server block for the challenge host
|
|
:rtype: list
|
|
"""
|
|
addrs = self._default_listen_addresses()
|
|
block = [['listen', ' ', addr.to_string(include_default=False)] for addr in addrs]
|
|
|
|
# Ensure we 404 on any other request by setting a root
|
|
document_root = os.path.join(
|
|
self.configurator.config.work_dir, "http_01_nonexistent")
|
|
|
|
block.extend([['server_name', ' ', achall.domain],
|
|
['root', ' ', document_root],
|
|
self._location_directive_for_achall(achall)
|
|
])
|
|
# TODO: do we want to return something else if they otherwise access this block?
|
|
return [['server'], block]
|
|
|
|
def _location_directive_for_achall(self, achall):
|
|
validation = achall.validation(achall.account_key)
|
|
validation_path = self._get_validation_path(achall)
|
|
|
|
location_directive = [['location', ' ', '=', ' ', validation_path],
|
|
[['default_type', ' ', 'text/plain'],
|
|
['return', ' ', '200', ' ', validation]]]
|
|
return location_directive
|
|
|
|
|
|
def _make_or_mod_server_block(self, achall: achallenges.KeyAuthorizationAnnotatedChallenge
|
|
) -> Optional[List]:
|
|
"""Modifies server blocks to respond to a challenge. Returns a new HTTP server block
|
|
to add to the configuration if an existing one can't be found.
|
|
|
|
:param achall: Annotated HTTP-01 challenge
|
|
:type achall: :class:`certbot.achallenges.KeyAuthorizationAnnotatedChallenge`
|
|
|
|
:returns: new server block to be added, if any
|
|
:rtype: list
|
|
|
|
"""
|
|
http_vhosts, https_vhosts = self.configurator.choose_auth_vhosts(achall.domain)
|
|
|
|
new_vhost: Optional[list] = None
|
|
if not http_vhosts:
|
|
# Couldn't find either a matching name+port server block
|
|
# or a port+default_server block, so create a dummy block
|
|
new_vhost = self._make_server_block(achall)
|
|
|
|
# Modify any existing server blocks
|
|
for vhost in set(http_vhosts + https_vhosts):
|
|
location_directive = [self._location_directive_for_achall(achall)]
|
|
|
|
self.configurator.parser.add_server_directives(vhost, location_directive)
|
|
|
|
rewrite_directive = [['rewrite', ' ', '^(/.well-known/acme-challenge/.*)',
|
|
' ', '$1', ' ', 'break']]
|
|
self.configurator.parser.add_server_directives(vhost,
|
|
rewrite_directive, insert_at_top=True)
|
|
|
|
return new_vhost
|