diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 36116f10e..8c5f4525f 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -9,13 +9,14 @@ import string import sys import M2Crypto +import zope.component from letsencrypt.client import acme from letsencrypt.client import challenge from letsencrypt.client import CONFIG from letsencrypt.client import crypto_util -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 network @@ -206,7 +207,8 @@ class Client(object): # sites may have been enabled / final cleanup self.installer.restart() - display.success_installation(self.names) + zope.component.getUtility( + interfaces.IDisplay).success_installation(self.names) return vhost @@ -223,7 +225,8 @@ class Client(object): """ if redirect is None: - redirect = display.redirect_by_default() + redirect = zope.component.getUtility( + interfaces.IDisplay).redirect_by_default() if redirect: self.redirect_to_ssl(vhost) diff --git a/letsencrypt/client/display.py b/letsencrypt/client/display.py index ac1f9d819..c6d90b5f0 100644 --- a/letsencrypt/client/display.py +++ b/letsencrypt/client/display.py @@ -1,73 +1,37 @@ import textwrap import dialog +import zope.interface + +from letsencrypt.client import interfaces WIDTH = 72 HEIGHT = 20 -class SingletonD(object): - _instance = None +class NcursesDisplay(object): + zope.interface.implements(interfaces.IDisplay) - def __new__(cls, *args, **kwargs): - if not cls._instance: - cls._instance = super(SingletonD, cls).__new__( - cls, *args, **kwargs) - return cls._instance - - -class Display(SingletonD): - """Generic display.""" - - def generic_notification(self, message, width=WIDTH, height=HEIGHT): - raise NotImplementedError() - - def generic_menu(self, message, choices, input_text="", - width=WIDTH, height=HEIGHT): - raise NotImplementedError() - - def generic_input(self, message): - raise NotImplementedError() - - def generic_yesno(self, message, yes_label="Yes", no_label="No"): - raise NotImplementedError() - - def filter_names(self, names): - raise NotImplementedError() - - def success_installation(self, domains): - raise NotImplementedError() - - def display_certs(self, certs): - raise NotImplementedError() - - def confirm_revocation(self, cert): - raise NotImplementedError() - - def more_info_cert(self, cert): - raise NotImplementedError() - - -class NcursesDisplay(Display): - - def __init__(self): + def __init__(self, width=WIDTH, height=HEIGHT): + super(NcursesDisplay, self).__init__() self.dialog = dialog.Dialog() + self.width = width + self.height = height - def generic_notification(self, message, w=WIDTH, h=HEIGHT): - self.dialog.msgbox(message, width=w, height=h) + def generic_notification(self, message): + self.dialog.msgbox(message, width=self.width) - def generic_menu(self, message, choices, input_text="", width=WIDTH, - height=HEIGHT): + def generic_menu(self, message, choices, input_text=""): # Can accept either tuples or just the actual choices if choices and isinstance(choices[0], tuple): code, selection = self.dialog.menu( - message, choices=choices, width=WIDTH, height=HEIGHT) + message, choices=choices, width=self.width, height=self.height) return code, str(selection) else: choices = list(enumerate(choices, 1)) code, tag = self.dialog.menu( - message, choices=choices, width=WIDTH, height=HEIGHT) + message, choices=choices, width=self.width, height=self.height) return code(int(tag) - 1) @@ -76,7 +40,7 @@ class NcursesDisplay(Display): def generic_yesno(self, message, yes="Yes", no="No"): return self.dialog.DIALOG_OK == self.dialog.yesno( - message, HEIGHT, WIDTH, yes_label=yes, no_label=no) + message, self.height, self.width, yes_label=yes, no_label=no) def filter_names(self, names): choices = [(n, "", 0) for n in names] @@ -88,12 +52,12 @@ class NcursesDisplay(Display): def success_installation(self, domains): self.dialog.msgbox( "\nCongratulations! You have successfully enabled " - + gen_https_names(domains) + "!", width=WIDTH) + + gen_https_names(domains) + "!", width=self.width) def display_certs(self, certs): list_choices = [ (str(i+1), "%s | %s | %s" % - (str(c["cn"].ljust(WIDTH - 39)), + (str(c["cn"].ljust(self.width - 39)), c["not_before"].strftime("%m-%d-%y"), "Installed" if c["installed"] else "")) for i, c in enumerate(certs)] @@ -102,7 +66,7 @@ class NcursesDisplay(Display): "Which certificates would you like to revoke?", choices=list_choices, help_button=True, help_label="More Info", ok_label="Revoke", - width=WIDTH, height=HEIGHT) + width=self.width, height=self.height) if not tag: tag = -1 return code, (int(tag) - 1) @@ -113,29 +77,45 @@ class NcursesDisplay(Display): text += cert_info_frame(cert) text += "This action cannot be reversed!" return self.dialog.DIALOG_OK == self.dialog.yesno( - text, width=WIDTH, height=HEIGHT) + text, width=self.width, height=self.height) def more_info_cert(self, cert): text = "Certificate Information:\n" text += cert_info_frame(cert) print text - self.dialog.msgbox(text, width=WIDTH, height=HEIGHT) + self.dialog.msgbox(text, width=self.width, height=self.height) + + def redirect_by_default(self): + choices = [ + ("Easy", "Allow both HTTP and HTTPS access to these sites"), + ("Secure", "Make all requests redirect to secure HTTPS access")] + + result = self.generic_menu( + "Please choose whether HTTPS access is required or optional.", + choices, "Please enter the appropriate number") + + if result[0] != OK: + return False + + # different answer for each type of display + return str(result[1]) == "Secure" or result[1] == 1 -class FileDisplay(Display): +class FileDisplay(object): + zope.interface.implements(interfaces.IDisplay) def __init__(self, outfile): + super(FileDisplay, self).__init__() self.outfile = outfile - def generic_notification(self, message, width=WIDTH, height=HEIGHT): + def generic_notification(self, message): side_frame = '-' * (79) wm = textwrap.fill(message, 80) text = "\n%s\n%s\n%s\n" % (side_frame, wm, side_frame) self.outfile.write(text) raw_input("Press Enter to Continue") - def generic_menu(self, message, choices, input_text="", - width=WIDTH, height=HEIGHT): + def generic_menu(self, message, choices, input_text=""): # 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] @@ -232,41 +212,11 @@ class FileDisplay(Display): self.outfile.write("\nCertificate Information:\n") self.outfile.write(cert_info_frame(cert)) -display = None OK = "ok" CANCEL = "cancel" HELP = "help" -def set_display(display_inst): - global display - display = display_inst - - -def generic_notification(message, width=WIDTH, height=HEIGHT): - display.generic_notification(message, width, height) - - -def generic_menu(message, choices, input_text="", width=WIDTH, height=HEIGHT): - return display.generic_menu(message, choices, input_text, width, height) - - -def generic_input(message): - return display.generic_message(message) - - -def generic_yesno(message, yes_label="Yes", no_label="No"): - return display.generic_yesno(message, yes_label, no_label) - - -def filter_names(names): - return display.filter_names(names) - - -def display_certs(certs): - return display.display_certs(certs) - - def cert_info_frame(cert): text = "-" * (WIDTH - 4) + "\n" text += cert_info_string(cert) @@ -303,33 +253,3 @@ def gen_https_names(domains): result = result + "https://" + domains[len(domains)-1] return result - - -def success_installation(domains): - return display.success_installation(domains) - - -def redirect_by_default(): - choices = [ - ("Easy", "Allow both HTTP and HTTPS access to these sites"), - ("Secure", "Make all requests redirect to secure HTTPS access")] - - result = display.generic_menu("Please choose whether HTTPS access " + - "is required or optional.", - choices, - "Please enter the appropriate number", - width=WIDTH) - - if result[0] != OK: - return False - - # different answer for each type of display - return str(result[1]) == "Secure" or result[1] == 1 - - -def confirm_revocation(cert): - return display.confirm_revocation(cert) - - -def more_info_cert(cert): - return display.more_info_cert(cert) diff --git a/letsencrypt/client/interfaces.py b/letsencrypt/client/interfaces.py index 3bdaace7f..910ec29c8 100644 --- a/letsencrypt/client/interfaces.py +++ b/letsencrypt/client/interfaces.py @@ -110,6 +110,40 @@ class IInstaller(zope.interface.Interface): """Restart or refresh the server content.""" +class IDisplay(zope.interface.Interface): + """Generic display.""" + + def generic_notification(message): + pass + + def generic_menu(message, choices, input_text=""): + pass + + def generic_input(message): + pass + + def generic_yesno(message, yes_label="Yes", no_label="No"): + pass + + def filter_names(names): + pass + + def success_installation(domains): + pass + + def display_certs(certs): + pass + + def confirm_revocation(cert): + pass + + def more_info_cert(cert): + pass + + def redirect_by_default(): + pass + + class IValidator(object): """Configuration validator.""" diff --git a/letsencrypt/client/recovery_token_challenge.py b/letsencrypt/client/recovery_token_challenge.py index abe8789b7..b10b24da2 100644 --- a/letsencrypt/client/recovery_token_challenge.py +++ b/letsencrypt/client/recovery_token_challenge.py @@ -3,9 +3,9 @@ .. note:: This challenge has not been implemented into the project yet """ +import zope.component import zope.interface -from letsencrypt.client import display from letsencrypt.client import interfaces @@ -22,8 +22,9 @@ class RecoveryToken(object): self.token = "" def perform(self, quiet=True): - cancel, self.token = display.generic_input( - "Please Input Recovery Token: ") + cancel, self.token = zope.component.getUtility( + interfaces.IDisplay).generic_input( + "Please Input Recovery Token: ") return cancel != 1 def cleanup(self): diff --git a/letsencrypt/client/revoker.py b/letsencrypt/client/revoker.py index 7fda722b8..073362501 100644 --- a/letsencrypt/client/revoker.py +++ b/letsencrypt/client/revoker.py @@ -5,11 +5,13 @@ import os import shutil import M2Crypto +import zope.component from letsencrypt.client import acme from letsencrypt.client import CONFIG from letsencrypt.client import crypto_util from letsencrypt.client import display +from letsencrypt.client import interfaces from letsencrypt.client import network @@ -35,9 +37,9 @@ class Revoker(object): revocation = self.network.send_and_receive_expected( acme.revocation_request(cert_der, key), "revocation") - display.generic_notification( + zope.component.getUtility(interfaces.IDisplay).generic_notification( "You have successfully revoked the certificate for " - "%s" % cert["cn"], width=70, height=9) + "%s" % cert["cn"]) self.remove_cert_key(cert) self.list_certs_keys() @@ -84,7 +86,7 @@ class Revoker(object): if certs: self.choose_certs(certs) else: - display.generic_notification( + zope.component.getUtility(interfaces.IDisplay).generic_notification( "There are not any trusted Let's Encrypt " "certificates for this server.") @@ -94,17 +96,18 @@ class Revoker(object): :param list certs: List of cert dicts. """ - code, tag = display.display_certs(certs) + displayer = zope.component.getUtility(interfaces.IDisplay) + code, tag = displayer.display_certs(certs) if code == display.OK: cert = certs[tag] - if display.confirm_revocation(cert): + if displayer.confirm_revocation(cert): self.acme_revocation(cert) else: self.choose_certs(certs) elif code == display.HELP: cert = certs[tag] - display.more_info_cert(cert) + displayer.more_info_cert(cert) self.choose_certs(certs) else: exit(0) diff --git a/letsencrypt/client/tests/apache_configurator_test.py b/letsencrypt/client/tests/apache_configurator_test.py index 20eb6e0c9..e1fd718a2 100644 --- a/letsencrypt/client/tests/apache_configurator_test.py +++ b/letsencrypt/client/tests/apache_configurator_test.py @@ -5,6 +5,7 @@ import shutil import unittest import mock +import zope.component from letsencrypt.client import display from letsencrypt.client import errors @@ -20,7 +21,7 @@ class TwoVhost80Test(unittest.TestCase): """Test two standard well configured HTTP vhosts.""" def setUp(self): - display.set_display(display.NcursesDisplay()) + zope.component.provideUtility(display.NcursesDisplay()) self.temp_dir, self.config_dir, self.work_dir = config_util.dir_setup( "debian_apache_2_4/two_vhost_80") diff --git a/letsencrypt/client/tests/apache_parser_test.py b/letsencrypt/client/tests/apache_parser_test.py index baf5c746e..340cdd324 100644 --- a/letsencrypt/client/tests/apache_parser_test.py +++ b/letsencrypt/client/tests/apache_parser_test.py @@ -5,6 +5,7 @@ import unittest import augeas import mock +import zope.component from letsencrypt.client import display from letsencrypt.client import errors @@ -15,7 +16,7 @@ from letsencrypt.client.tests import config_util class ApacheParserTest(unittest.TestCase): def setUp(self): - display.set_display(display.FileDisplay(sys.stdout)) + zope.component.provideUtility(display.FileDisplay(sys.stdout)) self.temp_dir, self.config_dir, self.work_dir = config_util.dir_setup( "debian_apache_2_4/two_vhost_80") diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 19c34cc64..8aeb43136 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -5,6 +5,8 @@ import logging import os import sys +import zope.component + from letsencrypt.client import CONFIG from letsencrypt.client import client from letsencrypt.client import display @@ -68,9 +70,10 @@ def main(): logger.setLevel(logging.INFO) if args.use_curses: logger.addHandler(log.DialogHandler()) - display.set_display(display.NcursesDisplay()) + displayer = display.NcursesDisplay() else: - display.set_display(display.FileDisplay(sys.stdout)) + displayer = display.FileDisplay(sys.stdout) + zope.component.provideUtility(displayer) installer = determine_installer() server = CONFIG.ACME_SERVER if args.server is None else args.server @@ -129,7 +132,7 @@ def main(): def display_eula(): """Displays the end user agreement.""" with open('EULA') as eula_file: - if not display.generic_yesno( + if not zope.component.getUtility(interfaces.IDisplay).generic_yesno( eula_file.read(), "Agree", "Cancel"): sys.exit(0) @@ -144,7 +147,8 @@ def choose_names(installer): # This function adds all names # found within the config to self.names # Then filters them based on user selection - code, names = display.filter_names(get_all_names(installer)) + code, names = zope.component.getUtility( + interfaces.IDisplay).filter_names(get_all_names(installer)) if code == display.OK and names: # TODO: Allow multiple names once it is setup return [names[0]] diff --git a/setup.py b/setup.py index f6d8f2880..c356e0841 100755 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ install_requires = [ 'python-augeas', 'python2-pythondialog', 'requests', + 'zope.component', 'zope.interface', ]