ops.py
1 """Contains UI methods for LE user operations.""" 2 import logging 3 import os 4 5 import zope.component 6 7 from letsencrypt import interfaces 8 from letsencrypt import le_util 9 from letsencrypt.display import util as display_util 10 11 12 logger = logging.getLogger(__name__) 13 14 # Define a helper function to avoid verbose code 15 util = zope.component.getUtility 16 17 18 def choose_plugin(prepared, question): 19 """Allow the user to choose their plugin. 20 21 :param list prepared: List of `~.PluginEntryPoint`. 22 :param str question: Question to be presented to the user. 23 24 :returns: Plugin entry point chosen by the user. 25 :rtype: `~.PluginEntryPoint` 26 27 """ 28 opts = [plugin_ep.description_with_name + 29 (" [Misconfigured]" if plugin_ep.misconfigured else "") 30 for plugin_ep in prepared] 31 32 while True: 33 code, index = util(interfaces.IDisplay).menu( 34 question, opts, help_label="More Info") 35 36 if code == display_util.OK: 37 return prepared[index] 38 elif code == display_util.HELP: 39 if prepared[index].misconfigured: 40 msg = "Reported Error: %s" % prepared[index].prepare() 41 else: 42 msg = prepared[index].init().more_info() 43 util(interfaces.IDisplay).notification( 44 msg, height=display_util.HEIGHT) 45 else: 46 return None 47 48 49 def pick_plugin(config, default, plugins, question, ifaces): 50 """Pick plugin. 51 52 :param letsencrypt.interfaces.IConfig: Configuration 53 :param str default: Plugin name supplied by user or ``None``. 54 :param letsencrypt.plugins.disco.PluginsRegistry plugins: 55 All plugins registered as entry points. 56 :param str question: Question to be presented to the user in case 57 multiple candidates are found. 58 :param list ifaces: Interfaces that plugins must provide. 59 60 :returns: Initialized plugin. 61 :rtype: IPlugin 62 63 """ 64 if default is not None: 65 # throw more UX-friendly error if default not in plugins 66 filtered = plugins.filter(lambda p_ep: p_ep.name == default) 67 else: 68 filtered = plugins.visible().ifaces(ifaces) 69 70 filtered.init(config) 71 verified = filtered.verify(ifaces) 72 verified.prepare() 73 prepared = verified.available() 74 75 if len(prepared) > 1: 76 logger.debug("Multiple candidate plugins: %s", prepared) 77 plugin_ep = choose_plugin(prepared.values(), question) 78 if plugin_ep is None: 79 return None 80 else: 81 return plugin_ep.init() 82 elif len(prepared) == 1: 83 plugin_ep = prepared.values()[0] 84 logger.debug("Single candidate plugin: %s", plugin_ep) 85 if plugin_ep.misconfigured: 86 return None 87 return plugin_ep.init() 88 else: 89 logger.debug("No candidate plugin") 90 return None 91 92 93 def pick_authenticator( 94 config, default, plugins, question="How would you " 95 "like to authenticate with the Let's Encrypt CA?"): 96 """Pick authentication plugin.""" 97 return pick_plugin( 98 config, default, plugins, question, (interfaces.IAuthenticator,)) 99 100 101 def pick_installer(config, default, plugins, 102 question="How would you like to install certificates?"): 103 """Pick installer plugin.""" 104 return pick_plugin( 105 config, default, plugins, question, (interfaces.IInstaller,)) 106 107 108 def pick_configurator( 109 config, default, plugins, 110 question="How would you like to authenticate and install " 111 "certificates?"): 112 """Pick configurator plugin.""" 113 return pick_plugin( 114 config, default, plugins, question, 115 (interfaces.IAuthenticator, interfaces.IInstaller)) 116 117 118 def get_email(): 119 """Prompt for valid email address. 120 121 :returns: Email or ``None`` if cancelled by user. 122 :rtype: str 123 124 """ 125 while True: 126 code, email = zope.component.getUtility(interfaces.IDisplay).input( 127 "Enter email address (used for urgent notices and lost key recovery)") 128 129 if code == display_util.OK: 130 if le_util.safe_email(email): 131 return email 132 else: 133 return None 134 135 136 def choose_account(accounts): 137 """Choose an account. 138 139 :param list accounts: Containing at least one 140 :class:`~letsencrypt.account.Account` 141 142 """ 143 # Note this will get more complicated once we start recording authorizations 144 labels = [acc.slug for acc in accounts] 145 146 code, index = util(interfaces.IDisplay).menu( 147 "Please choose an account", labels) 148 if code == display_util.OK: 149 return accounts[index] 150 else: 151 return None 152 153 154 def choose_names(installer): 155 """Display screen to select domains to validate. 156 157 :param installer: An installer object 158 :type installer: :class:`letsencrypt.interfaces.IInstaller` 159 160 :returns: List of selected names 161 :rtype: `list` of `str` 162 163 """ 164 if installer is None: 165 logger.debug("No installer, picking names manually") 166 return _choose_names_manually() 167 168 names = list(installer.get_all_names()) 169 170 if not names: 171 manual = util(interfaces.IDisplay).yesno( 172 "No names were found in your configuration files.{0}You should " 173 "specify ServerNames in your config files in order to allow for " 174 "accurate installation of your certificate.{0}" 175 "If you do use the default vhost, you may specify the name " 176 "manually. Would you like to continue?{0}".format(os.linesep)) 177 178 if manual: 179 return _choose_names_manually() 180 else: 181 return [] 182 183 code, names = _filter_names(names) 184 if code == display_util.OK and names: 185 return names 186 else: 187 return [] 188 189 190 def _filter_names(names): 191 """Determine which names the user would like to select from a list. 192 193 :param list names: domain names 194 195 :returns: tuple of the form (`code`, `names`) where 196 `code` - str display exit code 197 `names` - list of names selected 198 :rtype: tuple 199 200 """ 201 code, names = util(interfaces.IDisplay).checklist( 202 "Which names would you like to activate HTTPS for?", 203 tags=names) 204 return code, [str(s) for s in names] 205 206 207 def _choose_names_manually(): 208 """Manually input names for those without an installer.""" 209 210 code, input_ = util(interfaces.IDisplay).input( 211 "Please enter in your domain name(s) (comma and/or space separated) ") 212 213 if code == display_util.OK: 214 return display_util.separate_list_input(input_) 215 return [] 216 217 218 def success_installation(domains): 219 """Display a box confirming the installation of HTTPS. 220 221 .. todo:: This should be centered on the screen 222 223 :param list domains: domain names which were enabled 224 225 """ 226 util(interfaces.IDisplay).notification( 227 "Congratulations! You have successfully enabled {0}!{1}{1}" 228 "You should test your configuration at:{1}{2}".format( 229 _gen_https_names(domains), 230 os.linesep, 231 os.linesep.join(_gen_ssl_lab_urls(domains))), 232 height=(10 + len(domains)), 233 pause=False) 234 235 236 def success_renewal(domains): 237 """Display a box confirming the renewal of an existing certificate. 238 239 .. todo:: This should be centered on the screen 240 241 :param list domains: domain names which were renewed 242 243 """ 244 util(interfaces.IDisplay).notification( 245 "Your existing certificate has been successfully renewed, and the " 246 "new certificate has been installed.{1}{1}" 247 "The new certificate covers the following domains: {0}{1}{1}" 248 "You should test your configuration at:{1}{2}".format( 249 _gen_https_names(domains), 250 os.linesep, 251 os.linesep.join(_gen_ssl_lab_urls(domains))), 252 height=(14 + len(domains)), 253 pause=False) 254 255 256 def _gen_ssl_lab_urls(domains): 257 """Returns a list of urls. 258 259 :param list domains: Each domain is a 'str' 260 261 """ 262 return ["https://www.ssllabs.com/ssltest/analyze.html?d=%s" % dom for dom in domains] 263 264 265 def _gen_https_names(domains): 266 """Returns a string of the https domains. 267 268 Domains are formatted nicely with https:// prepended to each. 269 270 :param list domains: Each domain is a 'str' 271 272 """ 273 if len(domains) == 1: 274 return "https://{0}".format(domains[0]) 275 elif len(domains) == 2: 276 return "https://{dom[0]} and https://{dom[1]}".format(dom=domains) 277 elif len(domains) > 2: 278 return "{0}{1}{2}".format( 279 ", ".join("https://%s" % dom for dom in domains[:-1]), 280 ", and https://", 281 domains[-1]) 282 283 return ""