/ letsencrypt / display / ops.py
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 ""