mirror of
https://github.com/certbot/certbot.git
synced 2026-06-08 00:02:14 -04:00
refactor and enhance display, update revoker
This commit is contained in:
parent
e000cfd7c6
commit
168a70c273
18 changed files with 1174 additions and 315 deletions
|
|
@ -107,7 +107,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
self.check_parsing_errors("httpd.aug")
|
||||
|
||||
# Set Version
|
||||
self.version = self.get_version() if version is None else version
|
||||
self.version = get_version() if version is None else version
|
||||
|
||||
# Get all of the available vhosts
|
||||
self.vhosts = self.get_virtual_hosts()
|
||||
|
|
@ -911,37 +911,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
|
||||
return True
|
||||
|
||||
def get_version(self): # pylint: disable=no-self-use
|
||||
"""Return version of Apache Server.
|
||||
|
||||
Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7))
|
||||
|
||||
:returns: version
|
||||
:rtype: tuple
|
||||
|
||||
:raises errors.LetsEncryptConfiguratorError:
|
||||
Unable to find Apache version
|
||||
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[CONFIG.APACHE_CTL, '-v'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
text = proc.communicate()[0]
|
||||
except (OSError, ValueError):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to run %s -v" % CONFIG.APACHE_CTL)
|
||||
|
||||
regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
|
||||
matches = regex.findall(text)
|
||||
|
||||
if len(matches) != 1:
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to find Apache version")
|
||||
|
||||
return tuple([int(i) for i in matches[0].split('.')])
|
||||
|
||||
def verify_setup(self):
|
||||
"""Verify the setup to ensure safe operating environment.
|
||||
|
||||
|
|
@ -955,6 +924,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
le_util.make_or_verify_dir(self.direc["work"], 0o755, uid)
|
||||
le_util.make_or_verify_dir(self.direc["backup"], 0o755, uid)
|
||||
|
||||
@classmethod
|
||||
def __str__(cls):
|
||||
return "Apache version %s" % ".".join(get_version())
|
||||
|
||||
###########################################################################
|
||||
# Challenges Section
|
||||
###########################################################################
|
||||
|
|
@ -1012,6 +985,38 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
self.restart()
|
||||
|
||||
|
||||
def get_version(self):
|
||||
"""Return version of Apache Server.
|
||||
|
||||
Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7))
|
||||
|
||||
:returns: version
|
||||
:rtype: tuple
|
||||
|
||||
:raises errors.LetsEncryptConfiguratorError:
|
||||
Unable to find Apache version
|
||||
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[CONFIG.APACHE_CTL, '-v'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
text = proc.communicate()[0]
|
||||
except (OSError, ValueError):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to run %s -v" % CONFIG.APACHE_CTL)
|
||||
|
||||
regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
|
||||
matches = regex.findall(text)
|
||||
|
||||
if len(matches) != 1:
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to find Apache version")
|
||||
|
||||
return tuple([int(i) for i in matches[0].split('.')])
|
||||
|
||||
|
||||
def enable_mod(mod_name):
|
||||
"""Enables module in Apache.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
"""ACME protocol client class and helper functions."""
|
||||
import csv
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
import M2Crypto
|
||||
|
|
@ -21,6 +19,7 @@ from letsencrypt.client import reverter
|
|||
from letsencrypt.client import revoker
|
||||
|
||||
from letsencrypt.client.apache import configurator
|
||||
from letsencrypt.client.display import ops
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
|
@ -103,7 +102,7 @@ class Client(object):
|
|||
cert_file, chain_file = self.save_certificate(
|
||||
certificate_dict, cert_path, chain_path)
|
||||
|
||||
self.store_cert_key(cert_file, False)
|
||||
revoker.Revoker.store_cert_key(cert_file, False)
|
||||
|
||||
return cert_file, chain_file
|
||||
|
||||
|
|
@ -194,8 +193,7 @@ class Client(object):
|
|||
# sites may have been enabled / final cleanup
|
||||
self.installer.restart()
|
||||
|
||||
zope.component.getUtility(
|
||||
interfaces.IDisplay).success_installation(domains)
|
||||
ops.success_installation(domains)
|
||||
|
||||
def enhance_config(self, domains, redirect=None):
|
||||
"""Enhance the configuration.
|
||||
|
|
@ -225,52 +223,6 @@ class Client(object):
|
|||
if redirect:
|
||||
self.redirect_to_ssl(domains)
|
||||
|
||||
def store_cert_key(self, cert_file, encrypt=False):
|
||||
"""Store certificate key. (Used to allow quick revocation)
|
||||
|
||||
:param str cert_file: Path to a certificate file.
|
||||
|
||||
:param bool encrypt: Should the certificate key be encrypted?
|
||||
|
||||
:returns: True if key file was stored successfully, False otherwise.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST")
|
||||
le_util.make_or_verify_dir(CONFIG.CERT_KEY_BACKUP, 0o700)
|
||||
idx = 0
|
||||
|
||||
if encrypt:
|
||||
logging.error(
|
||||
"Unfortunately securely storing the certificates/"
|
||||
"keys is not yet available. Stay tuned for the "
|
||||
"next update!")
|
||||
return False
|
||||
|
||||
if os.path.isfile(list_file):
|
||||
with open(list_file, 'r+b') as csvfile:
|
||||
csvreader = csv.reader(csvfile)
|
||||
for row in csvreader:
|
||||
idx = int(row[0]) + 1
|
||||
csvwriter = csv.writer(csvfile)
|
||||
csvwriter.writerow([str(idx), cert_file, self.authkey.file])
|
||||
|
||||
else:
|
||||
with open(list_file, 'wb') as csvfile:
|
||||
csvwriter = csv.writer(csvfile)
|
||||
csvwriter.writerow(["0", cert_file, self.authkey.file])
|
||||
|
||||
shutil.copy2(self.authkey.file,
|
||||
os.path.join(
|
||||
CONFIG.CERT_KEY_BACKUP,
|
||||
os.path.basename(self.authkey.file) + "_" + str(idx)))
|
||||
shutil.copy2(cert_file,
|
||||
os.path.join(
|
||||
CONFIG.CERT_KEY_BACKUP,
|
||||
os.path.basename(cert_file) + "_" + str(idx)))
|
||||
|
||||
return True
|
||||
|
||||
def redirect_to_ssl(self, domains):
|
||||
"""Redirect all traffic from HTTP to HTTPS
|
||||
|
||||
|
|
@ -389,10 +341,13 @@ def csr_pem_to_der(csr):
|
|||
# This should be controlled by commandline parameters
|
||||
def determine_authenticator():
|
||||
"""Returns a valid IAuthenticator."""
|
||||
auths = []
|
||||
try:
|
||||
return configurator.ApacheConfigurator()
|
||||
auths.append(configurator.ApacheConfigurator())
|
||||
except errors.LetsEncryptNoInstallationError:
|
||||
logging.info("Unable to determine a way to authenticate the server")
|
||||
if len(auths) > 1:
|
||||
return ops.choose_authenticator(auths)
|
||||
|
||||
|
||||
def determine_installer():
|
||||
|
|
@ -484,7 +439,7 @@ def revoke(server):
|
|||
installer = None
|
||||
|
||||
revoc = revoker.Revoker(server, installer)
|
||||
revoc.list_certs_keys()
|
||||
revoc.display_menu()
|
||||
|
||||
|
||||
def view_config_changes():
|
||||
|
|
|
|||
378
letsencrypt/client/display/display_util.py
Normal file
378
letsencrypt/client/display/display_util.py
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
"""Lets Encrypt display."""
|
||||
import os
|
||||
import textwrap
|
||||
|
||||
import dialog
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt.client import interfaces
|
||||
|
||||
|
||||
WIDTH = 72
|
||||
HEIGHT = 20
|
||||
|
||||
# Display exit codes
|
||||
OK = "ok"
|
||||
"""Display exit code indicating user acceptance"""
|
||||
|
||||
CANCEL = "cancel"
|
||||
"""Display exit code for a user canceling the display"""
|
||||
|
||||
HELP = "help"
|
||||
"""Display exit code when for when the user requests more help."""
|
||||
|
||||
|
||||
class NcursesDisplay(object):
|
||||
"""Ncurses-based display."""
|
||||
|
||||
zope.interface.implements(interfaces.IDisplay)
|
||||
|
||||
def __init__(self, width=WIDTH, height=HEIGHT):
|
||||
super(NcursesDisplay, self).__init__()
|
||||
self.dialog = dialog.Dialog()
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def notification(self, message, height=10, pause=False):
|
||||
"""Display a notification to the user and wait for user acceptance.
|
||||
|
||||
:param str message: Message to display
|
||||
:param int height: Height of the dialog box
|
||||
:param bool pause: Not applicable to NcursesDisplay
|
||||
|
||||
"""
|
||||
self.dialog.msgbox(message, height, width=self.width)
|
||||
|
||||
def menu(self, message, choices,
|
||||
ok_label="OK", cancel_label="Cancel", help_label=""):
|
||||
"""Display a menu.
|
||||
|
||||
:param str message: title of menu
|
||||
|
||||
:param choices: menu lines
|
||||
:type choices: list of tuples (tag, item) or
|
||||
list of items (tags will be enumerated)
|
||||
|
||||
:param str ok_label: label of the OK button
|
||||
:param str help_label: label of the help button
|
||||
|
||||
:returns: tuple of the form (`code`, `tag`) where
|
||||
`code` - `str` display_util exit code
|
||||
`tag` - `str` or `int` index corresponding to the item chosen
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
if help_label:
|
||||
help_button = True
|
||||
else:
|
||||
help_button = False
|
||||
|
||||
# Can accept either tuples or just the actual choices
|
||||
if choices and isinstance(choices[0], tuple):
|
||||
code, selection = self.dialog.menu(
|
||||
message, choices=choices, ok_label=ok_label,
|
||||
cancel_label=cancel_label,
|
||||
help_button=help_button, help_label=help_label,
|
||||
width=self.width, height=self.height)
|
||||
|
||||
return code, str(selection)
|
||||
else:
|
||||
choices = [
|
||||
(str(i), choice) for i, choice in enumerate(choices, 1)
|
||||
]
|
||||
code, tag = self.dialog.menu(
|
||||
message, choices=choices, ok_label=ok_label,
|
||||
cancel_label=cancel_label,
|
||||
help_button=help_button, help_label=help_label,
|
||||
width=self.width, height=self.height)
|
||||
|
||||
return code, int(tag) - 1
|
||||
|
||||
def input(self, message):
|
||||
"""Display an input box to the user.
|
||||
|
||||
:param str message: Message to display that asks for input.
|
||||
|
||||
:returns: tuple of the form (code, string) where
|
||||
`code` - int display exit code
|
||||
`string` - input entered by the user
|
||||
|
||||
"""
|
||||
return self.dialog.inputbox(message)
|
||||
|
||||
def yesno(self, message, yes_label="Yes", no_label="No"):
|
||||
"""Display a Yes/No dialog box
|
||||
|
||||
:param str message: message to display to user
|
||||
:param str yes_label: label on the "yes" button
|
||||
:param str no_label: label on the "no" button
|
||||
|
||||
:returns: if yes_label was selected
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return self.dialog.DIALOG_OK == self.dialog.yesno(
|
||||
message, self.height, self.width,
|
||||
yes_label=yes_label, no_label=no_label)
|
||||
|
||||
def checklist(self, message, tags):
|
||||
"""Displays a checklist.
|
||||
|
||||
:param message: Message to display before choices
|
||||
:param list tags: where each is of type :class:`str`
|
||||
|
||||
:returns: tuple of the form (code, list_tags) where
|
||||
`code` - int display exit code
|
||||
`list_tags` - list of str tags selected by the user
|
||||
|
||||
"""
|
||||
choices = [(tag, "", False) for tag in tags]
|
||||
return self.dialog.checklist(
|
||||
message, width=self.width, height=self.height, choices=choices)
|
||||
|
||||
class FileDisplay(object):
|
||||
"""File-based display."""
|
||||
|
||||
zope.interface.implements(interfaces.IDisplay)
|
||||
|
||||
def __init__(self, outfile):
|
||||
super(FileDisplay, self).__init__()
|
||||
self.outfile = outfile
|
||||
|
||||
def notification(self, message, height=10, pause=True):
|
||||
"""Displays a notification and waits for user acceptance.
|
||||
|
||||
:param str message: Message to display
|
||||
:param int height: No effect for FileDisplay
|
||||
:param bool pause: Whether or not the program should pause for the
|
||||
user's confirmation
|
||||
|
||||
"""
|
||||
side_frame = "-" * 79
|
||||
message = self._wrap_lines(message)
|
||||
self.outfile.write(
|
||||
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
|
||||
line=os.linesep, frame=side_frame, msg=message))
|
||||
if pause:
|
||||
raw_input("Press Enter to Continue")
|
||||
|
||||
def menu(
|
||||
self, message, choices, ok_label="", cancel_label="", help_label=""):
|
||||
"""Display a menu.
|
||||
|
||||
:param str message: title of menu
|
||||
:param choices: Menu lines
|
||||
:type choices: list of tuples (tag, item) or
|
||||
list of descriptions (tags will be enumerated)
|
||||
|
||||
:returns: tuple of the form (code, tag) where
|
||||
code - int display exit code
|
||||
tag - str corresponding to the item chosen
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
self._print_menu(message, choices)
|
||||
|
||||
code, selection = self._get_valid_int_ans(len(choices))
|
||||
|
||||
return code, str(selection - 1)
|
||||
|
||||
def input(self, message):
|
||||
# pylint: disable=no-self-use
|
||||
"""Accept input from the user
|
||||
|
||||
:param str message: message to display to the user
|
||||
|
||||
:returns: tuple of (`code`, `input`) where
|
||||
`code` - str display exit code
|
||||
`input` - str of the user's input
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
ans = raw_input(
|
||||
textwrap.fill("%s (Enter c to cancel): " % message, 80))
|
||||
|
||||
if ans == "c" or ans == "C":
|
||||
return CANCEL, "-1"
|
||||
else:
|
||||
return OK, ans
|
||||
|
||||
def yesno(self, message, yes_label="Yes", no_label="No"):
|
||||
"""Query the user with a yes/no question.
|
||||
|
||||
: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
|
||||
|
||||
:returns: True for "Yes", False for "No"
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
side_frame = ("-" * 79) + os.linesep
|
||||
|
||||
message = self._wrap_lines(message)
|
||||
|
||||
self.outfile.write("{0}{frame}{msg}{0}{frame}".format(
|
||||
os.linesep, frame=side_frame, msg=message))
|
||||
|
||||
yes_label = _parens_around_char(yes_label)
|
||||
no_label = _parens_around_char(no_label)
|
||||
|
||||
ans = raw_input("{yes}/{no}: ".format(yes=yes_label, no=no_label))
|
||||
|
||||
return (ans.startswith(yes_label[0].lower()) or
|
||||
ans.startswith(yes_label[0].upper()))
|
||||
|
||||
def checklist(self, message, tags):
|
||||
"""Display a checklist.
|
||||
|
||||
:param str message: Message to display to user
|
||||
:param list tags: `str` tags to select
|
||||
|
||||
:returns: tuple of (`code`, `tags`) where
|
||||
`code` - str display exit code
|
||||
`tags` - list of selected tags
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
while True:
|
||||
self._print_menu(message, tags)
|
||||
|
||||
code, ans = self.input("Select the appropriate numbers "
|
||||
"separated by commas and/or spaces ")
|
||||
|
||||
if code == OK:
|
||||
indices = separate_list_input(ans)
|
||||
selected_tags = self._scrub_checklist_input(indices, tags)
|
||||
if selected_tags:
|
||||
return code, selected_tags
|
||||
else:
|
||||
self.outfile.write(
|
||||
"** Error - Invalid selection **%s" % os.linesep)
|
||||
else:
|
||||
return code, []
|
||||
|
||||
def _scrub_checklist_input(self, indices, tags):
|
||||
"""Validate input and transform indices to appropriate tags.
|
||||
|
||||
:param list indices: Checklist input
|
||||
:param list tags: Original tags of the checklist
|
||||
|
||||
:returns: tags the user selected
|
||||
:rtype: :class:`list` of :class:`str`
|
||||
|
||||
"""
|
||||
# They should all be of type int
|
||||
try:
|
||||
indices = [int(index) for index in indices]
|
||||
except TypeError:
|
||||
return []
|
||||
|
||||
# Remove duplicates
|
||||
indices = list(set(indices))
|
||||
|
||||
# Check all input is within range
|
||||
for index in indices:
|
||||
if index < 1 or index > len(tags):
|
||||
return []
|
||||
# Transform indices to appropriate tags
|
||||
return [tags[index-1] for index in indices]
|
||||
|
||||
|
||||
def _print_menu(self, message, choices):
|
||||
"""Print a menu on the screen.
|
||||
|
||||
:param str message: title of menu
|
||||
:param choices: Menu lines
|
||||
:type choices: list of tuples (tag, item) or
|
||||
list of descriptions (tags will be enumerated)
|
||||
|
||||
"""
|
||||
# Can take either tuples or single items in choices list
|
||||
if choices and isinstance(choices[0], tuple):
|
||||
choices = ["%s - %s" % (c[0], c[1]) for c in choices]
|
||||
|
||||
self.outfile.write(
|
||||
"{new}{msg}{new}".format(new=os.linesep, msg=message))
|
||||
side_frame = ("-" * 79) + os.linesep
|
||||
self.outfile.write(side_frame)
|
||||
|
||||
for i, tag in enumerate(choices, 1):
|
||||
self.outfile.write(
|
||||
textwrap.fill("{num}: {tag}".format(num=i, tag=tag), 80))
|
||||
|
||||
# Keep this outside of the textwrap
|
||||
self.outfile.write(os.linesep)
|
||||
|
||||
self.outfile.write(side_frame)
|
||||
|
||||
def _wrap_lines(self, msg): # pylint: disable=no-self-use
|
||||
"""Format lines nicely to 80 chars
|
||||
|
||||
:param str msg: Original message
|
||||
|
||||
:returns: Formatted message
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
lines = msg.splitlines()
|
||||
fixed_l = []
|
||||
for line in lines:
|
||||
fixed_l.append(textwrap.fill(line, 80))
|
||||
|
||||
return os.linesep.join(fixed_l)
|
||||
|
||||
def _get_valid_int_ans(self, max):
|
||||
"""Get a numerical selection.
|
||||
|
||||
:param int max: The maximum entry (len of choices)
|
||||
|
||||
:returns: tuple of the form (`code`, `selection`) where
|
||||
`code` - str display exit code ('ok' or cancel')
|
||||
`selection` - int user's selection
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
selection = -1
|
||||
if max > 1:
|
||||
input_msg = ("Select the appropriate number "
|
||||
"[1-{max}] then [enter] (press 'c' to "
|
||||
"cancel){end}".format(max=max, end=os.linesep))
|
||||
else:
|
||||
input_msg = ("Press 1 [enter] to confirm the selection "
|
||||
"(press 'c' to cancel){0}".format(os.linesep))
|
||||
while selection < 1:
|
||||
ans = raw_input(input_msg)
|
||||
if ans.startswith("c") or ans.startswith("C"):
|
||||
return CANCEL, -1
|
||||
try:
|
||||
selection = int(ans)
|
||||
if selection < 1 or selection > max:
|
||||
raise ValueError
|
||||
|
||||
except ValueError:
|
||||
self.outfile.write(
|
||||
"{0}** Invalid input **{0}".format(os.linesep))
|
||||
|
||||
return OK, selection
|
||||
|
||||
|
||||
def separate_list_input(input):
|
||||
"""Separate a comma or space separated list.
|
||||
|
||||
:param str input: input from the user
|
||||
|
||||
:returns: strings
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
no_commas = input.replace(",", " ")
|
||||
return [string for string in no_commas.split()]
|
||||
|
||||
def _parens_around_char(label):
|
||||
"""Place parens around first character of label.
|
||||
|
||||
:param str label: Must contain at least one character
|
||||
|
||||
"""
|
||||
return "({first}){rest}".format(first=label[0], rest=label[1:])
|
||||
68
letsencrypt/client/display/enhancements.py
Normal file
68
letsencrypt/client/display/enhancements.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
"""Let's Encrypt Enhancement Display"""
|
||||
import logging
|
||||
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client.display import display_util
|
||||
|
||||
|
||||
class EnhanceDisplay(object):
|
||||
"""Class used to display various enhancements.
|
||||
|
||||
.. note::This is not a subclass of Display. It merely uses Display as a
|
||||
component.
|
||||
|
||||
:ivar displayer: Display singleton
|
||||
:type displayer: :class:`letsencrypt.client.interfaces.IDisplay
|
||||
|
||||
:ivar dict dispatch: Dict mapping enhancements to functions
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
self.displayer = zope.component.getUtility(interfaces.IDisplay)
|
||||
|
||||
self.dispatch = {
|
||||
"redirect": self.redirect_by_default,
|
||||
}
|
||||
|
||||
def ask(self, enhancement):
|
||||
"""Display the enhancement to the user.
|
||||
|
||||
:param str enhancement: One of the
|
||||
:class:`letsencrypt.client.CONFIG.ENHANCEMENTS` enhancements
|
||||
|
||||
:returns: True if feature is desired, False otherwise
|
||||
:rtype: bool
|
||||
|
||||
:raises :class:`letsencrypt.client.errors.LetsEncryptClientError`: If
|
||||
the enhancement provided is not supported.
|
||||
|
||||
"""
|
||||
try:
|
||||
return self.dispatch[enhancement]
|
||||
except KeyError:
|
||||
logging.error("Unsupported enhancement given to ask()")
|
||||
raise errors.LetsEncryptClientError("Unsupported Enhancement")
|
||||
|
||||
def redirect_by_default(self):
|
||||
"""Determines whether the user would like to redirect to HTTPS.
|
||||
|
||||
:returns: True if redirect is desired, False otherwise
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
choices = [
|
||||
("Easy", "Allow both HTTP and HTTPS access to these sites"),
|
||||
("Secure", "Make all requests redirect to secure HTTPS access")]
|
||||
|
||||
result = self.displayer.menu(
|
||||
"Please choose whether HTTPS access is required or optional.",
|
||||
choices, "Please enter the appropriate number")
|
||||
|
||||
if result[0] != display_util.OK:
|
||||
return False
|
||||
|
||||
# different answer for each type of display
|
||||
return str(result[1]) == "Secure" or result[1] == 1
|
||||
124
letsencrypt/client/display/ops.py
Normal file
124
letsencrypt/client/display/ops.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client.display import display_util
|
||||
|
||||
# Define a helper function to avoid verbose code
|
||||
util = zope.component.getUtility
|
||||
|
||||
|
||||
def choose_authenticator(auths):
|
||||
"""Allow the user to choose their authenticator.
|
||||
|
||||
:param list auths: Where each is a
|
||||
:class:`letsencrypt.client.interfaces.IAuthenticator` object
|
||||
|
||||
:returns: Authenticator selected
|
||||
:rtype: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
|
||||
"""
|
||||
code, index = util(interfaces.IDisplay).menu(
|
||||
"How would you like to authenticate with the Let's Encrypt CA?",
|
||||
[str(auth.__class__) for auth in auths])
|
||||
|
||||
if code == display_util.OK:
|
||||
return auths[index]
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
def choose_names(installer):
|
||||
"""Display screen to select domains to validate.
|
||||
|
||||
:param installer: An installer object
|
||||
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
|
||||
|
||||
"""
|
||||
if installer is None:
|
||||
return _choose_names_manually()
|
||||
|
||||
names = list(installer.get_all_names())
|
||||
|
||||
if not names:
|
||||
manual = 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))
|
||||
|
||||
if manual:
|
||||
return _choose_names_manually()
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
code, names = _filter_names(names)
|
||||
if code == display_util.OK and names:
|
||||
return names
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
choices = [(n, "", 0) for n in names]
|
||||
code, names = util(interfaces.IDisplay).checklist(
|
||||
"Which names would you like to activate HTTPS for?",
|
||||
choices=choices)
|
||||
return code, [str(s) for s in names]
|
||||
|
||||
|
||||
def _choose_names_manually():
|
||||
"""Manualy input names for those without an installer."""
|
||||
|
||||
code, input = util(interfaces.IDisplay).input(
|
||||
"Please enter in your domain name(s) (comma and/or space separated) ")
|
||||
|
||||
if code == display_util.OK:
|
||||
return display_util.separate_list_input(input)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def success_installation(domains):
|
||||
"""Display a box confirming the installation of HTTPS.
|
||||
|
||||
:param list domains: domain names which were enabled
|
||||
|
||||
"""
|
||||
util(interfaces.IDisplay).notification(
|
||||
"Congratulations! You have successfully enabled "
|
||||
"%s!" % _gen_https_names(domains), pause=True)
|
||||
|
||||
|
||||
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://" + dom for dom in domains[:-1]),
|
||||
", and https://",
|
||||
domains[-1])
|
||||
|
||||
return ""
|
||||
106
letsencrypt/client/display/revocation.py
Normal file
106
letsencrypt/client/display/revocation.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import os
|
||||
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client.display import display_util
|
||||
|
||||
util = zope.component.getUtility
|
||||
|
||||
|
||||
def choose_certs(certs):
|
||||
"""Display choose certificates menu.
|
||||
|
||||
:param list certs: List of cert dicts.
|
||||
|
||||
:returns: cert to revoke
|
||||
:rtype: :class:`letsencrypt.client.revoker.Cert`
|
||||
|
||||
"""
|
||||
code, tag = display_certs(certs)
|
||||
|
||||
if code == display_util.OK:
|
||||
cert = certs[tag]
|
||||
if confirm_revocation(cert):
|
||||
return cert
|
||||
else:
|
||||
choose_certs(certs)
|
||||
elif code == display_util.HELP:
|
||||
cert = certs[tag]
|
||||
more_info_cert(cert)
|
||||
choose_certs(certs)
|
||||
else:
|
||||
exit(0)
|
||||
|
||||
|
||||
def display_certs(certs):
|
||||
"""Display the certificates in a menu for revocation.
|
||||
|
||||
:param list certs: each is a :class:`letsencrypt.client.revoker.Cert`
|
||||
|
||||
:returns: tuple of the form (code, selection) where
|
||||
code is a display exit code
|
||||
selection is the user's int selection
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
list_choices = [
|
||||
("%s | %s | %s" %
|
||||
(str(cert.get_cn().ljust(display_util.WIDTH - 39)),
|
||||
cert.get_not_before().strftime("%m-%d-%y"),
|
||||
"Installed" if cert.installed and cert.installed != ["Unknown"]
|
||||
else "")
|
||||
for cert in enumerate(certs))
|
||||
]
|
||||
|
||||
code, tag = util(interfaces.IDisplay).menu(
|
||||
"Which certificates would you like to revoke?",
|
||||
"Revoke number (c to cancel): ",
|
||||
choices=list_choices, help_button=True,
|
||||
help_label="More Info", ok_label="Revoke",
|
||||
cancel_label="Exit")
|
||||
if not tag:
|
||||
tag = -1
|
||||
|
||||
return code, (int(tag) - 1)
|
||||
|
||||
|
||||
def confirm_revocation(self, cert):
|
||||
"""Confirm revocation screen.
|
||||
|
||||
:param cert: certificate object
|
||||
:type cert: :class:
|
||||
|
||||
:returns: True if user would like to revoke, False otherwise
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
text = ("{0}Are you sure you would like to revoke the following "
|
||||
"certificate:{0}".format(os.linesep))
|
||||
text += cert.pretty_print()
|
||||
text += "This action cannot be reversed!"
|
||||
return display_util.OK == util(interfaces.IDisplay).yesno(
|
||||
text, width=display_util.WIDTH, height=display_util.HEIGHT)
|
||||
|
||||
|
||||
def more_info_cert(cert):
|
||||
"""Displays more info about the cert.
|
||||
|
||||
:param dict cert: cert dict used throughout revoker.py
|
||||
|
||||
"""
|
||||
text = "{0}Certificate Information:{0}".format(os.linesep)
|
||||
text += cert.pretty_print()
|
||||
util(interfaces.IDisplay).notification(text, height=display_util.HEIGHT)
|
||||
|
||||
|
||||
def success_revocation(cert):
|
||||
"""Display a success message.
|
||||
|
||||
:param cert: cert that was revoked
|
||||
:type cert: :class:`letsencrypt.client.revoker.Cert`
|
||||
|
||||
"""
|
||||
util(interfaces.IDisplay).notification(
|
||||
"You have successfully revoked the certificate for "
|
||||
"%s" % cert.get_cn())
|
||||
|
|
@ -45,19 +45,6 @@ class IAuthenticator(zope.interface.Interface):
|
|||
"""Revert changes and shutdown after challenges complete."""
|
||||
|
||||
|
||||
class IChallenge(zope.interface.Interface):
|
||||
"""Let's Encrypt challenge."""
|
||||
|
||||
def perform():
|
||||
"""Perform the challenge."""
|
||||
|
||||
def generate_response():
|
||||
"""Generate response."""
|
||||
|
||||
def cleanup():
|
||||
"""Cleanup."""
|
||||
|
||||
|
||||
class IInstaller(zope.interface.Interface):
|
||||
"""Generic Let's Encrypt Installer Interface.
|
||||
|
||||
|
|
@ -144,14 +131,17 @@ class IInstaller(zope.interface.Interface):
|
|||
class IDisplay(zope.interface.Interface):
|
||||
"""Generic display."""
|
||||
|
||||
def generic_notification(message):
|
||||
def notification(message, height, pause):
|
||||
"""Displays a string message
|
||||
|
||||
:param str message: Message to display
|
||||
:param int height: Height of dialog box if applicable
|
||||
:param bool pause: Whether or not the application should pause for
|
||||
confirmation (if available)
|
||||
|
||||
"""
|
||||
|
||||
def generic_menu(message, choices, input_text=""):
|
||||
def menu(message, choices, input_text="", ok_label="OK", help_label=""):
|
||||
"""Displays a generic menu.
|
||||
|
||||
:param str message: message to display
|
||||
|
|
@ -163,29 +153,37 @@ class IDisplay(zope.interface.Interface):
|
|||
|
||||
"""
|
||||
|
||||
def generic_input(message):
|
||||
"""Accept input from the user."""
|
||||
def input(message):
|
||||
"""Accept input from the user
|
||||
|
||||
def generic_yesno(message, yes_label="Yes", no_label="No"):
|
||||
"""A yes/no dialog."""
|
||||
:param str message: message to display to the user
|
||||
|
||||
def filter_names(names):
|
||||
"""Allow the user to select which names they would like to activate."""
|
||||
:returns: tuple of (`code`, `input`) where
|
||||
`code` - str display exit code
|
||||
`input` - str of the user's input
|
||||
:rtype: tuple
|
||||
|
||||
def success_installation(domains):
|
||||
"""Display a congratulations message for new https domains."""
|
||||
"""
|
||||
|
||||
def display_certs(certs):
|
||||
"""Display a list of certificates."""
|
||||
def yesno(message, yes_label="Yes", no_label="No"):
|
||||
"""Query the user with a yes/no question.
|
||||
|
||||
def confirm_revocation(cert):
|
||||
"""Confirmation of revocation screen."""
|
||||
:param str message: question for the user
|
||||
|
||||
def more_info_cert(cert):
|
||||
"""Print out all information for a given certificate dict."""
|
||||
:returns: True for "Yes", False for "No"
|
||||
:rtype: bool
|
||||
|
||||
def redirect_by_default():
|
||||
"""Ask the user whether they would like to redirect to HTTPS."""
|
||||
"""
|
||||
|
||||
def checkbox(message, choices):
|
||||
"""Allow for multiple selections from a menu.
|
||||
|
||||
:param str message: message to display to the user
|
||||
|
||||
:param choices: :param choices: choices
|
||||
:type choices: :class:`list` of :func:`tuple`
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class IValidator(zope.interface.Interface):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import logging
|
|||
|
||||
import dialog
|
||||
|
||||
from letsencrypt.client import display
|
||||
from letsencrypt.client.display import display_util
|
||||
|
||||
|
||||
class DialogHandler(logging.Handler): # pylint: disable=too-few-public-methods
|
||||
|
|
@ -19,8 +19,8 @@ class DialogHandler(logging.Handler): # pylint: disable=too-few-public-methods
|
|||
PADDING_HEIGHT = 2
|
||||
PADDING_WIDTH = 4
|
||||
|
||||
def __init__(self, level=logging.NOTSET, height=display.HEIGHT,
|
||||
width=display.WIDTH - 4, d=None):
|
||||
def __init__(self, level=logging.NOTSET, height=display_util.HEIGHT,
|
||||
width=display_util.WIDTH - 4, d=None):
|
||||
# Handler not new-style -> no super
|
||||
logging.Handler.__init__(self, level)
|
||||
self.height = height
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import time
|
|||
import zope.component
|
||||
|
||||
from letsencrypt.client import CONFIG
|
||||
from letsencrypt.client import display
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
from letsencrypt.client.display import display_util
|
||||
|
||||
|
||||
class Reverter(object):
|
||||
|
|
@ -127,7 +127,7 @@ class Reverter(object):
|
|||
output.append(os.linesep)
|
||||
|
||||
zope.component.getUtility(interfaces.IDisplay).generic_notification(
|
||||
os.linesep.join(output), display.HEIGHT)
|
||||
os.linesep.join(output), display_util.HEIGHT)
|
||||
|
||||
def add_to_temp_checkpoint(self, save_files, save_notes):
|
||||
"""Add files to temporary checkpoint
|
||||
|
|
|
|||
|
|
@ -6,60 +6,74 @@ import os
|
|||
import shutil
|
||||
|
||||
import M2Crypto
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.client import acme
|
||||
from letsencrypt.client import CONFIG
|
||||
from letsencrypt.client import display
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
from letsencrypt.client import network
|
||||
|
||||
from letsencrypt.client.display import display_util
|
||||
from letsencrypt.client.display import revocation
|
||||
|
||||
|
||||
class Revoker(object):
|
||||
"""A revocation class for LE."""
|
||||
|
||||
list_path = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST")
|
||||
|
||||
def __init__(self, server, installer):
|
||||
self.network = network.Network(server)
|
||||
self.installer = installer
|
||||
self.displayer = zope.component.getUtility(interfaces.IDisplay)
|
||||
|
||||
def acme_revocation(self, cert):
|
||||
"""Handle ACME "revocation" phase.
|
||||
|
||||
:param dict cert: TODO
|
||||
:param cert: cert intended to be revoked
|
||||
:type cert: :class:`letsencrypt.client.revoker.Cert`
|
||||
|
||||
:returns: ACME "revocation" message.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
cert_der = M2Crypto.X509.load_cert(cert["backup_cert_file"]).as_der()
|
||||
with open(cert["backup_key_file"], "rU") as backup_key_file:
|
||||
with open(cert.backup_key_path, "rU") as backup_key_file:
|
||||
key = backup_key_file.read()
|
||||
|
||||
revocation = self.network.send_and_receive_expected(
|
||||
revoc = self.network.send_and_receive_expected(
|
||||
acme.revocation_request(cert_der, key), "revocation")
|
||||
|
||||
self.displayer.notification(
|
||||
"You have successfully revoked the certificate for "
|
||||
"%s" % cert["cn"])
|
||||
revocation.success_revocation(cert)
|
||||
|
||||
self.remove_cert_key(cert)
|
||||
self.list_certs_keys()
|
||||
self.display_menu()
|
||||
|
||||
return revocation
|
||||
return revoc
|
||||
|
||||
def list_certs_keys(self):
|
||||
def display_menu(self):
|
||||
"""List trusted Let's Encrypt certificates."""
|
||||
list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST")
|
||||
certs = []
|
||||
|
||||
if not os.path.isfile(list_file):
|
||||
if not os.path.isfile(Revoker.list_path):
|
||||
logging.info(
|
||||
"You don't have any certificates saved from letsencrypt")
|
||||
return
|
||||
|
||||
csha1_vhlist = self._get_installed_locations()
|
||||
certs = self._populate_saved_certs(csha1_vhlist)
|
||||
|
||||
with open(list_file, "rb") as csvfile:
|
||||
if certs:
|
||||
self._insert_installed_status(certs)
|
||||
cert = self.choose_certs(certs)
|
||||
self.acme_revocation(cert)
|
||||
else:
|
||||
logging.info(
|
||||
"There are not any trusted Let's Encrypt "
|
||||
"certificates for this server.")
|
||||
|
||||
def _populate_saved_certs(self, csha1_vhlist):
|
||||
"""Populate a list of all the saved certs."""
|
||||
|
||||
certs = []
|
||||
with open(Revoker.list_path, "rb") as csvfile:
|
||||
csvreader = csv.reader(csvfile)
|
||||
# idx, orig_cert, orig_key
|
||||
for row in csvreader:
|
||||
|
|
@ -78,16 +92,16 @@ class Revoker(object):
|
|||
cert.get_fingerprint, [])
|
||||
|
||||
certs.append(cert)
|
||||
if certs:
|
||||
self._insert_installed_status(certs)
|
||||
self.choose_certs(certs)
|
||||
else:
|
||||
self.displayer.notification(
|
||||
"There are not any trusted Let's Encrypt "
|
||||
"certificates for this server.")
|
||||
|
||||
return certs
|
||||
|
||||
def _get_installed_locations(self):
|
||||
"""Get installed locations of certificates"""
|
||||
"""Get installed locations of certificates
|
||||
|
||||
:returns: cert sha1 fingerprint -> :class:`list` of vhosts where
|
||||
the certificate is installed.
|
||||
|
||||
"""
|
||||
csha1_vhlist = {}
|
||||
|
||||
if self.installer is None:
|
||||
|
|
@ -106,40 +120,27 @@ class Revoker(object):
|
|||
|
||||
return csha1_vhlist
|
||||
|
||||
def choose_certs(self, certs):
|
||||
"""Display choose certificates menu.
|
||||
|
||||
:param list certs: List of cert dicts.
|
||||
|
||||
"""
|
||||
code, tag = self.display_certs(certs)
|
||||
|
||||
if code == display.OK:
|
||||
cert = certs[tag]
|
||||
if self.confirm_revocation(cert):
|
||||
self.acme_revocation(cert)
|
||||
else:
|
||||
self.choose_certs(certs)
|
||||
elif code == display.HELP:
|
||||
cert = certs[tag]
|
||||
self.displayer.more_info_cert(cert)
|
||||
self.choose_certs(certs)
|
||||
else:
|
||||
exit(0)
|
||||
|
||||
def remove_cert_key(self, cert): # pylint: disable=no-self-use
|
||||
"""Remove certificate and key.
|
||||
|
||||
:param cert: Cert dict used throughout revocation
|
||||
:param cert: cert object
|
||||
:type cert: :class:`letsencrypt.client.revoker.Cert`
|
||||
|
||||
"""
|
||||
list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST")
|
||||
list_file2 = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST.tmp")
|
||||
self._remove_cert_from_list(cert)
|
||||
|
||||
with open(list_file, "rb") as orgfile:
|
||||
# Remove files
|
||||
os.remove(cert["backup_cert_file"])
|
||||
os.remove(cert["backup_key_file"])
|
||||
|
||||
def _remove_cert_from_list(self, cert):
|
||||
"""Remove a certificate from the LIST file."""
|
||||
list_path2 = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST.tmp")
|
||||
|
||||
with open(Revoker.list_path, "rb") as orgfile:
|
||||
csvreader = csv.reader(orgfile)
|
||||
|
||||
with open(list_file2, "wb") as newfile:
|
||||
with open(list_path2, "wb") as newfile:
|
||||
csvwriter = csv.writer(newfile)
|
||||
|
||||
for row in csvreader:
|
||||
|
|
@ -148,67 +149,62 @@ class Revoker(object):
|
|||
row[2] == cert.orig_key.path):
|
||||
csvwriter.writerow(row)
|
||||
|
||||
shutil.copy2(list_file2, list_file)
|
||||
os.remove(list_file2)
|
||||
os.remove(cert["backup_cert_file"])
|
||||
os.remove(cert["backup_key_file"])
|
||||
shutil.copy2(list_path2, Revoker.list_path)
|
||||
os.remove(list_path2)
|
||||
|
||||
def display_certs(self, certs):
|
||||
"""Display the certificates in a menu for revocation.
|
||||
@classmethod
|
||||
def store_cert_key(cls, cert_path, key_path, encrypt=False):
|
||||
"""Store certificate key. (Used to allow quick revocation)
|
||||
|
||||
:param list certs: `list` of :class:`letsencrypt.client.
|
||||
:param str cert_path: Path to a certificate file.
|
||||
:param key_path: Authorized key for certificate
|
||||
:type key_path: :class:`letsencrypt.client.le_util.Key`
|
||||
|
||||
:returns: tuple of the form (code, selection) where
|
||||
code is a display exit code
|
||||
selection is the user's int selection
|
||||
:rtype: tuple
|
||||
:param bool encrypt: Should the certificate key be encrypted?
|
||||
|
||||
"""
|
||||
list_choices = [
|
||||
("%s | %s | %s" %
|
||||
(str(cert.get_cn().ljust(display.WIDTH - 39)),
|
||||
cert.get_not_before().strftime("%m-%d-%y"),
|
||||
"Installed" if cert.installed and cert.installed != ["Unknown"]
|
||||
else "")
|
||||
for cert in enumerate(certs))
|
||||
]
|
||||
|
||||
code, tag = self.displayer.menu(
|
||||
"Which certificates would you like to revoke?",
|
||||
"Revoke number (c to cancel): ",
|
||||
choices=list_choices, help_button=True,
|
||||
help_label="More Info", ok_label="Revoke")
|
||||
if not tag:
|
||||
tag = -1
|
||||
|
||||
return code, (int(tag) - 1)
|
||||
|
||||
def confirm_revocation(self, cert):
|
||||
"""Confirm revocation screen.
|
||||
|
||||
:param cert: certificate object
|
||||
:type cert: :class:
|
||||
|
||||
:returns: True if user would like to revoke, False otherwise
|
||||
:returns: True if key file was stored successfully, False otherwise.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
text = ("{0}Are you sure you would like to revoke the following "
|
||||
"certificate:{0}".format(os.linesep))
|
||||
text += cert.pretty_print()
|
||||
text += "This action cannot be reversed!"
|
||||
return display.OK == self.dialog.yesno(
|
||||
text, width=self.width, height=self.height)
|
||||
le_util.make_or_verify_dir(CONFIG.CERT_KEY_BACKUP, 0o700)
|
||||
idx = 0
|
||||
|
||||
def more_info_cert(self, cert):
|
||||
"""Displays more info about the cert.
|
||||
if encrypt:
|
||||
logging.error(
|
||||
"Unfortunately securely storing the certificates/"
|
||||
"keys is not yet available. Stay tuned for the "
|
||||
"next update!")
|
||||
return False
|
||||
|
||||
:param dict cert: cert dict used throughout revoker.py
|
||||
cls._append_index_file(cert_path, key_path)
|
||||
|
||||
"""
|
||||
text = "{0}Certificate Information:{0}".format(os.linesep)
|
||||
text += cert.pretty_print()
|
||||
self.notification(text, height=self.height)
|
||||
shutil.copy2(key_path,
|
||||
os.path.join(
|
||||
CONFIG.CERT_KEY_BACKUP,
|
||||
os.path.basename(key_path) + "_" + str(idx)))
|
||||
shutil.copy2(cert_path,
|
||||
os.path.join(
|
||||
CONFIG.CERT_KEY_BACKUP,
|
||||
os.path.basename(cert_path) + "_" + str(idx)))
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def _append_index_file(cls, cert_path, key_path):
|
||||
if os.path.isfile(Revoker.list_path):
|
||||
with open(Revoker.list_path, 'r+b') as csvfile:
|
||||
csvreader = csv.reader(csvfile)
|
||||
|
||||
# Find the highest index in the file
|
||||
for row in csvreader:
|
||||
idx = int(row[0]) + 1
|
||||
csvwriter = csv.writer(csvfile)
|
||||
csvwriter.writerow([str(idx), cert_path, key_path])
|
||||
|
||||
else:
|
||||
with open(Revoker.list_path, 'wb') as csvfile:
|
||||
csvwriter = csv.writer(csvfile)
|
||||
csvwriter.writerow(["0", cert_path, key_path])
|
||||
|
||||
|
||||
class Cert(object):
|
||||
|
|
@ -232,7 +228,7 @@ class Cert(object):
|
|||
PathStatus = collections.namedtuple("PathStatus", "path status")
|
||||
"""Convenience container to hold path and status info"""
|
||||
|
||||
def __init__(self, cert_filepath):
|
||||
def __init__(self, cert_path):
|
||||
"""Cert initialization
|
||||
|
||||
:param str cert_filepath: Name of file containing certificate in
|
||||
|
|
@ -240,7 +236,7 @@ class Cert(object):
|
|||
|
||||
"""
|
||||
try:
|
||||
self.cert = M2Crypto.X509.load_cert(cert_filepath)
|
||||
self.cert = M2Crypto.X509.load_cert(cert_path)
|
||||
except (IOError, M2Crypto.X509.X509Error):
|
||||
self.cert = None
|
||||
|
||||
|
|
@ -329,7 +325,6 @@ class Cert(object):
|
|||
return ""
|
||||
|
||||
def __str__(self):
|
||||
"""Turn a Certinto a string."""
|
||||
text = []
|
||||
text.append("Subject: %s" % self.get_subject())
|
||||
text.append("SAN: %s" % self.get_san())
|
||||
|
|
@ -344,8 +339,8 @@ class Cert(object):
|
|||
|
||||
def pretty_print(self):
|
||||
"""Nicely frames a cert str"""
|
||||
text = "-" * (display.WIDTH - 4) + os.linesep
|
||||
text = "-" * (display_util.WIDTH - 4) + os.linesep
|
||||
text += str(self)
|
||||
text += "-" * (display.WIDTH - 4)
|
||||
text += "-" * (display_util.WIDTH - 4)
|
||||
return text
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import augeas
|
|||
import mock
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.client import display
|
||||
from letsencrypt.client.display import display_util
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client.apache import parser
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ class ApacheParserTest(util.ApacheTest):
|
|||
def setUp(self):
|
||||
super(ApacheParserTest, self).setUp()
|
||||
|
||||
zope.component.provideUtility(display.FileDisplay(sys.stdout))
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
|
||||
self.parser = parser.ApacheParser(
|
||||
augeas.Augeas(flags=augeas.Augeas.NONE),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class DvsniGenCertTest(unittest.TestCase):
|
|||
r_b64 = le_util.jose_b64encode(dvsni_r)
|
||||
pem = pkg_resources.resource_string(
|
||||
__name__, os.path.join("testdata", "rsa256_key.pem"))
|
||||
key = client.Client.Key("path", pem)
|
||||
key = le_util.Client.Key("path", pem)
|
||||
nonce = "12345ABCDE"
|
||||
cert_pem, s_b64 = self._call(domain, r_b64, nonce, key)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class RollbackTest(unittest.TestCase):
|
|||
def test_misconfiguration_fixed(self, mock_det, mock_rev, mock_input):
|
||||
mock_det.side_effect = [errors.LetsEncryptMisconfigurationError,
|
||||
self.m_install]
|
||||
self.m_input().yesno.return_value = True
|
||||
mock_input().yesno.return_value = True
|
||||
|
||||
self._call(1)
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ class RollbackTest(unittest.TestCase):
|
|||
self, mock_det, mock_rev, mock_warn, mock_input):
|
||||
mock_det.side_effect = errors.LetsEncryptMisconfigurationError
|
||||
|
||||
self.m_input().yesno.return_value = True
|
||||
mock_input().yesno.return_value = True
|
||||
|
||||
self._call(1)
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ class RollbackTest(unittest.TestCase):
|
|||
self, mock_det, mock_rev, mock_input):
|
||||
mock_det.side_effect = errors.LetsEncryptMisconfigurationError
|
||||
|
||||
self.m_input().yesno.return_value = False
|
||||
mock_input().yesno.return_value = False
|
||||
|
||||
self._call(1)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
"""Tests for letsencrypt.client.crypto_util."""
|
||||
import datetime
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
|
@ -132,42 +131,42 @@ class MakeSSCertTest(unittest.TestCase):
|
|||
make_ss_cert(RSA256_KEY, ['example.com', 'www.example.com'])
|
||||
|
||||
|
||||
class GetCertInfoTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.crypto_util.get_cert_info."""
|
||||
|
||||
def setUp(self):
|
||||
self.cert_info = {
|
||||
'not_before': datetime.datetime(
|
||||
2014, 12, 11, 22, 34, 45, tzinfo=M2Crypto.ASN1.UTC),
|
||||
'not_after': datetime.datetime(
|
||||
2014, 12, 18, 22, 34, 45, tzinfo=M2Crypto.ASN1.UTC),
|
||||
'subject': 'C=US, ST=Michigan, L=Ann Arbor, O=University '
|
||||
'of Michigan and the EFF, CN=example.com',
|
||||
'cn': 'example.com',
|
||||
'issuer': 'C=US, ST=Michigan, L=Ann Arbor, O=University '
|
||||
'of Michigan and the EFF, CN=example.com',
|
||||
'serial': 1337L,
|
||||
'pub_key': 'RSA 512',
|
||||
}
|
||||
|
||||
def _call(self, name):
|
||||
from letsencrypt.client.crypto_util import get_cert_info
|
||||
self.assertEqual(get_cert_info(pkg_resources.resource_filename(
|
||||
__name__, os.path.join('testdata', name))), self.cert_info)
|
||||
|
||||
def test_single_domain(self):
|
||||
self.cert_info.update({
|
||||
'san': '',
|
||||
'fingerprint': '9F8CE01450D288467C3326AC0457E351939C72E',
|
||||
})
|
||||
self._call('cert.pem')
|
||||
|
||||
def test_san(self):
|
||||
self.cert_info.update({
|
||||
'san': 'DNS:example.com, DNS:www.example.com',
|
||||
'fingerprint': '62F7110431B8E8F55905DBE5592518F9634AC50A',
|
||||
})
|
||||
self._call('cert-san.pem')
|
||||
# class GetCertInfoTest(unittest.TestCase):
|
||||
# """Tests for letsencrypt.client.crypto_util.get_cert_info."""
|
||||
#
|
||||
# def setUp(self):
|
||||
# self.cert_info = {
|
||||
# 'not_before': datetime.datetime(
|
||||
# 2014, 12, 11, 22, 34, 45, tzinfo=M2Crypto.ASN1.UTC),
|
||||
# 'not_after': datetime.datetime(
|
||||
# 2014, 12, 18, 22, 34, 45, tzinfo=M2Crypto.ASN1.UTC),
|
||||
# 'subject': 'C=US, ST=Michigan, L=Ann Arbor, O=University '
|
||||
# 'of Michigan and the EFF, CN=example.com',
|
||||
# 'cn': 'example.com',
|
||||
# 'issuer': 'C=US, ST=Michigan, L=Ann Arbor, O=University '
|
||||
# 'of Michigan and the EFF, CN=example.com',
|
||||
# 'serial': 1337L,
|
||||
# 'pub_key': 'RSA 512',
|
||||
# }
|
||||
#
|
||||
# def _call(self, name):
|
||||
# from letsencrypt.client.crypto_util import get_cert_info
|
||||
# self.assertEqual(get_cert_info(pkg_resources.resource_filename(
|
||||
# __name__, os.path.join('testdata', name))), self.cert_info)
|
||||
#
|
||||
# def test_single_domain(self):
|
||||
# self.cert_info.update({
|
||||
# 'san': '',
|
||||
# 'fingerprint': '9F8CE01450D288467C3326AC0457E351939C72E',
|
||||
# })
|
||||
# self._call('cert.pem')
|
||||
#
|
||||
# def test_san(self):
|
||||
# self.cert_info.update({
|
||||
# 'san': 'DNS:example.com, DNS:www.example.com',
|
||||
# 'fingerprint': '62F7110431B8E8F55905DBE5592518F9634AC50A',
|
||||
# })
|
||||
# self._call('cert-san.pem')
|
||||
|
||||
|
||||
class B64CertToPEMTest(unittest.TestCase):
|
||||
|
|
|
|||
105
letsencrypt/client/tests/display/display_util_test.py
Normal file
105
letsencrypt/client/tests/display/display_util_test.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from letsencrypt.client.display import display_util
|
||||
|
||||
|
||||
class DisplayT(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.choices = [("First", "Description1"), ("Second", "Description2")]
|
||||
self.tags = ["tag1", "tag2", "tag3"]
|
||||
|
||||
|
||||
def test_visual(displayer, choices):
|
||||
"""Visually test all of the display functions."""
|
||||
displayer.notification("Random notification!")
|
||||
displayer.menu("Question?", choices,
|
||||
ok_label="O", cancel_label="Can", help_label="??")
|
||||
displayer.menu("Question?", [choice[1] for choice in choices],
|
||||
ok_label="O", cancel_label="Can", help_label="??")
|
||||
displayer.input("Input Message")
|
||||
displayer.yesno(
|
||||
"Yes/No Message", yes_label="Yessir", no_label="Nosir")
|
||||
displayer.checklist(
|
||||
"Checklist Message", [choice[0] for choice in choices])
|
||||
|
||||
|
||||
class NcursesDisplayTest(DisplayT):
|
||||
"""Test ncurses display."""
|
||||
def setUp(self):
|
||||
super(NcursesDisplayTest, self).setUp()
|
||||
self.displayer = display_util.NcursesDisplay()
|
||||
|
||||
@mock.patch("letsencrypt.client.display.display_util.dialog.Dialog.msgbox")
|
||||
def test_notification(self, mock_msgbox):
|
||||
"""Kind of worthless... one liner."""
|
||||
self.displayer.notification("message")
|
||||
self.assertEqual(mock_msgbox.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.client.display.display_util.dialog.Dialog.menu")
|
||||
def test_menu(self, mock_menu):
|
||||
pass
|
||||
|
||||
def test_visual(self):
|
||||
test_visual(self.displayer, self.choices)
|
||||
|
||||
|
||||
class FileOutputDisplayTest(DisplayT):
|
||||
"""Test stdout display."""
|
||||
def setUp(self):
|
||||
super(FileOutputDisplayTest, self).setUp()
|
||||
self.displayer = display_util.FileDisplay(sys.stdout)
|
||||
|
||||
def test_visual(self):
|
||||
test_visual(self.displayer, self.choices)
|
||||
|
||||
|
||||
class SeparateListInputTest(unittest.TestCase):
|
||||
"""Test Module functions."""
|
||||
def setUp(self):
|
||||
self.exp = ["a", "b", "c", "test"]
|
||||
|
||||
@classmethod
|
||||
def _call(cls, input):
|
||||
from letsencrypt.client.display.display_util import separate_list_input
|
||||
return separate_list_input(input)
|
||||
|
||||
def test_commas(self):
|
||||
actual = self._call("a,b,c,test")
|
||||
self.assertEqual(actual, self.exp)
|
||||
|
||||
def test_spaces(self):
|
||||
actual = self._call("a b c test")
|
||||
self.assertEqual(actual, self.exp)
|
||||
|
||||
def test_both(self):
|
||||
actual = self._call("a, b, c, test")
|
||||
self.assertEqual(actual, self.exp)
|
||||
|
||||
def test_mess(self):
|
||||
actual = [self._call(" a , b c \t test")]
|
||||
actual.append(self._call(",a, ,, , b c test "))
|
||||
|
||||
for act in actual:
|
||||
self.assertEqual(act, self.exp)
|
||||
|
||||
|
||||
class PlaceParensTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def _call(cls, label):
|
||||
from letsencrypt.client.display.display_util import _parens_around_char
|
||||
return _parens_around_char(label)
|
||||
|
||||
def test_single_letter(self):
|
||||
ret = self._call("a")
|
||||
self.assertEqual("(a)", ret)
|
||||
|
||||
def test_multiple(self):
|
||||
ret = self._call("Label")
|
||||
self.assertEqual("(L)abel", ret)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
159
letsencrypt/client/tests/display/ops_test.py
Normal file
159
letsencrypt/client/tests/display/ops_test.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.client.display import display_util
|
||||
|
||||
|
||||
class ChooseAuthenticatorTest(unittest.TestCase):
|
||||
"""Test choose_authenticator function."""
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
|
||||
@classmethod
|
||||
def _call(cls, auths):
|
||||
from letsencrypt.client.display.ops import choose_authenticator
|
||||
return choose_authenticator(auths)
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_successful_choice(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.OK, 0)
|
||||
|
||||
ret = self._call(["authenticator1", "auth2"])
|
||||
|
||||
self.assertEqual(ret, "authenticator1")
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_no_choice(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.CANCEL, 0)
|
||||
|
||||
self.assertRaises(SystemExit, self._call, ["authenticator1"])
|
||||
|
||||
|
||||
class GenHttpsNamesTest(unittest.TestCase):
|
||||
"""Test _gen_https_names"""
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
|
||||
@classmethod
|
||||
def _call(cls, domains):
|
||||
from letsencrypt.client.display.ops import _gen_https_names
|
||||
return _gen_https_names(domains)
|
||||
|
||||
def test_zero(self):
|
||||
self.assertEqual(self._call([]), "")
|
||||
|
||||
def test_one(self):
|
||||
dom = "example.com"
|
||||
self.assertEqual(self._call([dom]), "https://%s" % dom)
|
||||
|
||||
def test_two(self):
|
||||
doms = ["foo.bar.org", "bar.org"]
|
||||
self.assertEqual(
|
||||
self._call(doms),
|
||||
"https://{dom[0]} and https://{dom[1]}".format(dom=doms))
|
||||
|
||||
def test_three(self):
|
||||
doms = ["a.org", "b.org", "c.org"]
|
||||
# We use an oxford comma
|
||||
self.assertEqual(
|
||||
self._call(doms),
|
||||
"https://{dom[0]}, https://{dom[1]}, and https://{dom[2]}".format(
|
||||
dom=doms))
|
||||
|
||||
def test_four(self):
|
||||
doms = ["a.org", "b.org", "c.org", "d.org"]
|
||||
exp = ("https://{dom[0]}, https://{dom[1]}, https://{dom[2]}, "
|
||||
"and https://{dom[3]}".format(dom=doms))
|
||||
|
||||
self.assertEqual(self._call(doms), exp)
|
||||
|
||||
|
||||
class ChooseNamesTest(unittest.TestCase):
|
||||
"""Test choose names."""
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
self.mock_install = mock.MagicMock()
|
||||
|
||||
@classmethod
|
||||
def _call(cls, installer):
|
||||
from letsencrypt.client.display.ops import choose_names
|
||||
return choose_names(installer)
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops._choose_names_manually")
|
||||
def test_no_installer(self, mock_manual):
|
||||
self._call(None)
|
||||
self.assertEqual(mock_manual.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_no_installer_cancel(self, mock_util):
|
||||
mock_util().input.return_value = (display_util.CANCEL, [])
|
||||
self.assertRaises(SystemExit, self._call, None)
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_no_names_choose(self, mock_util):
|
||||
self.mock_install().get_all_names.return_value = set()
|
||||
mock_util().yesno.return_value = True
|
||||
domain = "example.com"
|
||||
mock_util().input.return_value = (display_util.OK, domain)
|
||||
|
||||
actual_doms = self._call(self.mock_install)
|
||||
self.assertEqual(mock_util().input.call_count, 1)
|
||||
self.assertEqual(actual_doms, [domain])
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_no_names_quit(self, mock_util):
|
||||
self.mock_install().get_all_names.return_value = set()
|
||||
mock_util().yesno.return_value = False
|
||||
|
||||
self.assertRaises(SystemExit, self._call, self.mock_install)
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_filter_names_valid_return(self, mock_util):
|
||||
self.mock_install.get_all_names.return_value = set(["example.com"])
|
||||
mock_util().checklist.return_value = (display_util.OK, ["example.com"])
|
||||
|
||||
names = self._call(self.mock_install)
|
||||
self.assertEqual(names, ["example.com"])
|
||||
self.assertEqual(mock_util().checklist.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_filter_names_nothing_selected(self, mock_util):
|
||||
self.mock_install.get_all_names.return_value = set(["example.com"])
|
||||
mock_util().checklist.return_value = (display_util.OK, [])
|
||||
|
||||
self.assertRaises(SystemExit, self._call, self.mock_install)
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_filter_names_cancel(self, mock_util):
|
||||
self.mock_install.get_all_names.return_value = set(["example.com"])
|
||||
mock_util().checklist.return_value = (display_util.CANCEL, ["example.com"])
|
||||
|
||||
self.assertRaises(SystemExit, self._call, self.mock_install)
|
||||
|
||||
|
||||
class SuccessInstallationTest(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def _call(cls, names):
|
||||
from letsencrypt.client.display.ops import success_installation
|
||||
success_installation(names)
|
||||
|
||||
@mock.patch("letsencrypt.client.display.ops.util")
|
||||
def test_success_installation(self, mock_util):
|
||||
mock_util().notification.return_value = None
|
||||
names = ["example.com", "abc.com"]
|
||||
|
||||
self._call(names)
|
||||
|
||||
self.assertEqual(mock_util().notification.call_count, 1)
|
||||
arg = mock_util().notification.call_args_list[0][0][0]
|
||||
|
||||
for name in names:
|
||||
self.assertTrue(name in arg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -15,11 +15,12 @@ import zope.interface
|
|||
import letsencrypt
|
||||
from letsencrypt.client import CONFIG
|
||||
from letsencrypt.client import client
|
||||
from letsencrypt.client import display
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
from letsencrypt.client import log
|
||||
from letsencrypt.client.display import display_util
|
||||
from letsencrypt.client.display import ops
|
||||
|
||||
|
||||
def main(): # pylint: disable=too-many-statements,too-many-branches
|
||||
|
|
@ -77,9 +78,9 @@ def main(): # pylint: disable=too-many-statements,too-many-branches
|
|||
logger.setLevel(logging.INFO)
|
||||
if args.use_curses:
|
||||
logger.addHandler(log.DialogHandler())
|
||||
displayer = display.NcursesDisplay()
|
||||
displayer = display_util.NcursesDisplay()
|
||||
else:
|
||||
displayer = display.FileDisplay(sys.stdout)
|
||||
displayer = display_util.FileDisplay(sys.stdout)
|
||||
zope.component.provideUtility(displayer)
|
||||
|
||||
if args.view_config_changes:
|
||||
|
|
@ -100,11 +101,11 @@ def main(): # pylint: disable=too-many-statements,too-many-branches
|
|||
# Make sure we actually get an installer that is functioning properly
|
||||
# before we begin to try to use it.
|
||||
try:
|
||||
installer = client.determine_installer()
|
||||
installer = client.determine_authenticator()
|
||||
except errors.LetsEncryptMisconfigurationError as err:
|
||||
logging.fatal("Please fix your configuration before proceeding. "
|
||||
"The Installer exited with the following message: "
|
||||
"%s", err)
|
||||
logging.fatal("Please fix your configuration before proceeding.{0}"
|
||||
"The Authenticator exited with the following message: "
|
||||
"{1}".format(os.linesep, err))
|
||||
sys.exit(1)
|
||||
|
||||
# Use the same object if possible
|
||||
|
|
@ -113,7 +114,7 @@ def main(): # pylint: disable=too-many-statements,too-many-branches
|
|||
else:
|
||||
auth = client.determine_authenticator()
|
||||
|
||||
domains = choose_names(installer) if args.domains is None else args.domains
|
||||
domains = ops.choose_names(installer) if args.domains is None else args.domains
|
||||
|
||||
# Prepare for init of Client
|
||||
if args.privkey is None:
|
||||
|
|
@ -146,42 +147,6 @@ def display_eula():
|
|||
sys.exit(0)
|
||||
|
||||
|
||||
def choose_names(installer):
|
||||
"""Display screen to select domains to validate.
|
||||
|
||||
:param installer: An installer object
|
||||
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
|
||||
|
||||
"""
|
||||
# This function adds all names found in the installer configuration
|
||||
# Then filters them based on user selection
|
||||
code, names = zope.component.getUtility(
|
||||
interfaces.IDisplay).filter_names(get_all_names(installer))
|
||||
if code == display.OK and names:
|
||||
return names
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def get_all_names(installer):
|
||||
"""Return all valid names in the configuration.
|
||||
|
||||
:param installer: An installer object
|
||||
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
|
||||
|
||||
"""
|
||||
names = list(installer.get_all_names())
|
||||
|
||||
if not names:
|
||||
logging.fatal("No domain names were found in your installation")
|
||||
logging.fatal("Either specify which names you would like "
|
||||
"letsencrypt to validate or add server names "
|
||||
"to your virtual hosts")
|
||||
sys.exit(1)
|
||||
|
||||
return names
|
||||
|
||||
|
||||
def read_file(filename):
|
||||
"""Returns the given file's contents with universal new line support.
|
||||
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -61,8 +61,10 @@ setup(
|
|||
'letsencrypt',
|
||||
'letsencrypt.client',
|
||||
'letsencrypt.client.apache',
|
||||
'letsencrypt.client.display',
|
||||
'letsencrypt.client.tests',
|
||||
'letsencrypt.client.tests.apache',
|
||||
'letsencrypt.client.tests.display',
|
||||
'letsencrypt.scripts',
|
||||
],
|
||||
install_requires=install_requires,
|
||||
|
|
|
|||
Loading…
Reference in a new issue