Files

259 lines
8.1 KiB
Python

"""Contains UI methods for LE user operations."""
import logging
import os
import zope.component
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt import le_util
from letsencrypt.display import util as display_util
logger = logging.getLogger(__name__)
# Define a helper function to avoid verbose code
z_util = zope.component.getUtility
def get_email(more=False, invalid=False):
"""Prompt for valid email address.
:param bool more: explain why the email is strongly advisable, but how to
skip it
:param bool invalid: true if the user just typed something, but it wasn't
a valid-looking email
:returns: Email or ``None`` if cancelled by user.
:rtype: str
"""
msg = "Enter email address (used for urgent notices and lost key recovery)"
if invalid:
msg = "There seem to be problems with that address. " + msg
if more:
msg += ('\n\nIf you really want to skip this, you can run the client with '
'--register-unsafely-without-email but make sure you backup your '
'account key from /etc/letsencrypt/accounts\n\n')
try:
code, email = zope.component.getUtility(interfaces.IDisplay).input(msg)
except errors.MissingCommandlineFlag:
msg = ("You should register before running non-interactively, or provide --agree-tos"
" and --email <email_address> flags")
raise errors.MissingCommandlineFlag(msg)
if code == display_util.OK:
if le_util.safe_email(email):
return email
else:
# TODO catch the server's ACME invalid email address error, and
# make a similar call when that happens
return get_email(more=True, invalid=(email != ""))
else:
return None
def choose_account(accounts):
"""Choose an account.
:param list accounts: Containing at least one
:class:`~letsencrypt.account.Account`
"""
# Note this will get more complicated once we start recording authorizations
labels = [acc.slug for acc in accounts]
code, index = z_util(interfaces.IDisplay).menu(
"Please choose an account", labels)
if code == display_util.OK:
return accounts[index]
else:
return None
def choose_names(installer):
"""Display screen to select domains to validate.
:param installer: An installer object
:type installer: :class:`letsencrypt.interfaces.IInstaller`
:returns: List of selected names
:rtype: `list` of `str`
"""
if installer is None:
logger.debug("No installer, picking names manually")
return _choose_names_manually()
domains = list(installer.get_all_names())
names = get_valid_domains(domains)
if not names:
manual = z_util(interfaces.IDisplay).yesno(
"No names were found in your configuration files.{0}You should "
"specify ServerNames in your config files in order to allow for "
"accurate installation of your certificate.{0}"
"If you do use the default vhost, you may specify the name "
"manually. Would you like to continue?{0}".format(os.linesep),
default=True)
if manual:
return _choose_names_manually()
else:
return []
code, names = _filter_names(names)
if code == display_util.OK and names:
return names
else:
return []
def get_valid_domains(domains):
"""Helper method for choose_names that implements basic checks
on domain names
:param list domains: Domain names to validate
:return: List of valid domains
:rtype: list
"""
valid_domains = []
for domain in domains:
try:
valid_domains.append(le_util.enforce_domain_sanity(domain))
except errors.ConfigurationError:
continue
return valid_domains
def _filter_names(names):
"""Determine which names the user would like to select from a list.
:param list names: domain names
:returns: tuple of the form (`code`, `names`) where
`code` - str display exit code
`names` - list of names selected
:rtype: tuple
"""
code, names = z_util(interfaces.IDisplay).checklist(
"Which names would you like to activate HTTPS for?",
tags=names, cli_flag="--domains")
return code, [str(s) for s in names]
def _choose_names_manually():
"""Manually input names for those without an installer."""
code, input_ = z_util(interfaces.IDisplay).input(
"Please enter in your domain name(s) (comma and/or space separated) ",
cli_flag="--domains")
if code == display_util.OK:
invalid_domains = dict()
retry_message = ""
try:
domain_list = display_util.separate_list_input(input_)
except UnicodeEncodeError:
domain_list = []
retry_message = (
"Internationalized domain names are not presently "
"supported.{0}{0}Would you like to re-enter the "
"names?{0}").format(os.linesep)
for i, domain in enumerate(domain_list):
try:
domain_list[i] = le_util.enforce_domain_sanity(domain)
except errors.ConfigurationError as e:
invalid_domains[domain] = e.message
if len(invalid_domains):
retry_message = (
"One or more of the entered domain names was not valid:"
"{0}{0}").format(os.linesep)
for domain in invalid_domains:
retry_message = retry_message + "{1}: {2}{0}".format(
os.linesep, domain, invalid_domains[domain])
retry_message = retry_message + (
"{0}Would you like to re-enter the names?{0}").format(
os.linesep)
if retry_message:
# We had error in input
retry = z_util(interfaces.IDisplay).yesno(retry_message)
if retry:
return _choose_names_manually()
else:
return domain_list
return []
def success_installation(domains):
"""Display a box confirming the installation of HTTPS.
.. todo:: This should be centered on the screen
:param list domains: domain names which were enabled
"""
z_util(interfaces.IDisplay).notification(
"Congratulations! You have successfully enabled {0}{1}{1}"
"You should test your configuration at:{1}{2}".format(
_gen_https_names(domains),
os.linesep,
os.linesep.join(_gen_ssl_lab_urls(domains))),
height=(10 + len(domains)),
pause=False)
def success_renewal(domains, action):
"""Display a box confirming the renewal of an existing certificate.
.. todo:: This should be centered on the screen
:param list domains: domain names which were renewed
:param str action: can be "reinstall" or "renew"
"""
z_util(interfaces.IDisplay).notification(
"Your existing certificate has been successfully {3}ed, and the "
"new certificate has been installed.{1}{1}"
"The new certificate covers the following domains: {0}{1}{1}"
"You should test your configuration at:{1}{2}".format(
_gen_https_names(domains),
os.linesep,
os.linesep.join(_gen_ssl_lab_urls(domains)),
action),
height=(14 + len(domains)),
pause=False)
def _gen_ssl_lab_urls(domains):
"""Returns a list of urls.
:param list domains: Each domain is a 'str'
"""
return ["https://www.ssllabs.com/ssltest/analyze.html?d=%s" % dom for dom in domains]
def _gen_https_names(domains):
"""Returns a string of the https domains.
Domains are formatted nicely with https:// prepended to each.
:param list domains: Each domain is a 'str'
"""
if len(domains) == 1:
return "https://{0}".format(domains[0])
elif len(domains) == 2:
return "https://{dom[0]} and https://{dom[1]}".format(dom=domains)
elif len(domains) > 2:
return "{0}{1}{2}".format(
", ".join("https://%s" % dom for dom in domains[:-1]),
", and https://",
domains[-1])
return ""