[tor-commits] [arm/master] Wizard config for setting up an exit

atagar at torproject.org atagar at torproject.org
Mon Jun 27 17:00:33 UTC 2011


commit e8c3836e203a3e359a9b5b9a289dc73ddc3da92c
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Jun 27 08:54:07 2011 -0700

    Wizard config for setting up an exit
    
    This expands the wizard to provide a config panel for setting up an exit relay.
    This has the same options as an internal relay plus several for
    DirPortFrontPage and ExitPolicy.
    
    By default this will use the reduced exit policy [1], with options like Vidalia
    for selecting groups of services to allow. However, unlike Vidalia it won't
    allow the operator to pick plaintext ports without also having their encryped
    counterparts.
    
    [1] https://trac.torproject.org/projects/tor/wiki/doc/ReducedExitPolicy
---
 src/cli/wizard.py |  155 ++++++++++++++++++++++++++++++++++++++++++++--------
 src/settings.cfg  |   37 ++++++++++++-
 2 files changed, 167 insertions(+), 25 deletions(-)

diff --git a/src/cli/wizard.py b/src/cli/wizard.py
index dadea16..0d47de5 100644
--- a/src/cli/wizard.py
+++ b/src/cli/wizard.py
@@ -3,6 +3,7 @@ Provides user prompts for setting up a new relay. This autogenerates a torrc
 that's used by arm to run its own tor instance.
 """
 
+import functools
 import curses
 
 import cli.popups
@@ -14,15 +15,30 @@ from util import enum, uiTools
 RelayType = enum.Enum("RELAY", "EXIT", "BRIDGE", "CLIENT")
 
 # all options that can be configured
-Options = enum.Enum("DIVIDER", "NICKNAME", "CONTACT", "NOTIFY", "BANDWIDTH", "LIMIT", "STARTUP")
+Options = enum.Enum("DIVIDER", "NICKNAME", "CONTACT", "NOTIFY", "BANDWIDTH", "LIMIT", "STARTUP", "NOTICE", "POLICY", "WEBSITES", "EMAIL", "IM", "MISC", "PLAINTEXT")
 RelayOptions = {RelayType.RELAY: (Options.NICKNAME,
                                   Options.CONTACT,
                                   Options.NOTIFY,
                                   Options.BANDWIDTH,
-                                  Options.DIVIDER,
-                                  Options.DIVIDER,
                                   Options.LIMIT,
-                                  Options.STARTUP)}
+                                  Options.STARTUP),
+                RelayType.EXIT:  (Options.NICKNAME,
+                                  Options.CONTACT,
+                                  Options.NOTIFY,
+                                  Options.BANDWIDTH,
+                                  Options.LIMIT,
+                                  Options.STARTUP,
+                                  Options.DIVIDER,
+                                  Options.NOTICE,
+                                  Options.POLICY,
+                                  Options.WEBSITES,
+                                  Options.EMAIL,
+                                  Options.IM,
+                                  Options.MISC,
+                                  Options.PLAINTEXT)}
+
+# custom exit policy options
+CUSTOM_POLICIES = (Options.WEBSITES, Options.EMAIL, Options.IM, Options.MISC, Options.PLAINTEXT)
 
 # other options provided in the prompts
 CANCEL, NEXT, BACK = "Cancel", "Next", "Back"
@@ -30,10 +46,13 @@ CANCEL, NEXT, BACK = "Cancel", "Next", "Back"
 DESC_SIZE = 5 # height of the description field
 MSG_COLOR = "green"
 OPTION_COLOR = "yellow"
+DISABLED_COLOR = "cyan"
 
 CONFIG = {"wizard.message.role": "",
           "wizard.message.relay": "",
+          "wizard.message.exit": "",
           "wizard.toggle": {},
+          "wizard.suboptions": [],
           "wizard.default": {},
           "wizard.label.general": {},
           "wizard.label.role": {},
@@ -65,6 +84,8 @@ class ConfigOption:
     self.descriptionCache = None
     self.descriptionCacheArg = None
     self.value = default
+    self.validator = None
+    self._isEnabled = True
   
   def getKey(self):
     return self.key
@@ -75,7 +96,39 @@ class ConfigOption:
   def getDisplayValue(self):
     return self.value
   
+  def getDisplayAttr(self):
+    myColor = OPTION_COLOR if self.isEnabled() else DISABLED_COLOR
+    return curses.A_BOLD | uiTools.getColor(myColor)
+  
+  def isEnabled(self):
+    return self._isEnabled
+  
+  def setEnabled(self, isEnabled):
+    self._isEnabled = isEnabled
+  
+  def setValidator(self, validator):
+    """
+    Custom function used to check that a value is valid before setting it.
+    This functor should accept two arguments: this option and the value we're
+    attempting to set. If its invalid then a ValueError with the reason is
+    expected.
+    
+    Arguments:
+      validator - functor for checking the validitiy of values we set
+    """
+    
+    self.validator = validator
+  
   def setValue(self, value):
+    """
+    Attempts to set our value. If a validator has been set then we first check
+    if it's alright, raising a ValueError with the reason if not.
+    
+    Arguments:
+      value - value we're attempting to set
+    """
+    
+    if self.validator: self.validator(self, value)
     self.value = value
   
   def getLabel(self, prefix = ""):
@@ -90,6 +143,10 @@ class ConfigOption:
     return [prefix + line for line in self.descriptionCache]
 
 class ToggleConfigOption(ConfigOption):
+  """
+  Configuration option representing a boolean.
+  """
+  
   def __init__(self, key, group, default, trueLabel, falseLabel):
     ConfigOption.__init__(self, key, group, default)
     self.trueLabel = trueLabel
@@ -99,11 +156,20 @@ class ToggleConfigOption(ConfigOption):
     return self.trueLabel if self.value else self.falseLabel
   
   def toggle(self):
+    # This isn't really here to validate the value (after all this is a
+    # boolean, the options are limited!), but rather give a method for functors
+    # to be triggered when selected.
+    
+    if self.validator: self.validator(self, not self.value)
     self.value = not self.value
 
 def showWizard():
-  relayType, config = None, {}
+  """
+  Provides a series of prompts, allowing the user to spawn a customized tor
+  instance.
+  """
   
+  relayType, config = None, {}
   for option in Options.values():
     if option == Options.DIVIDER:
       config[option] = option
@@ -121,12 +187,22 @@ def showWizard():
       config[option] = ToggleConfigOption(option, "opt", isSet, trueLabel.strip(), falseLabel.strip())
     else: config[option] = ConfigOption(option, "opt", default)
   
+  # sets input validators
+  
+  # enables custom policies when 'custom' is selected and disables otherwise
+  policyOpt = config[Options.POLICY]
+  policyOpt.setValidator(functools.partial(_exitPolicyAction, config))
+  _exitPolicyAction(config, policyOpt, policyOpt.getValue())
+  
+  # remembers the last selection made on the type prompt page
+  relaySelection = RelayType.RELAY
+  
   while True:
     if relayType == None:
-      selection = promptRelayType()
+      selection = promptRelayType(relaySelection)
       
       if selection == CANCEL: break
-      else: relayType = selection
+      else: relayType, relaySelection = selection, selection
     else:
       selection = promptConfigOptions(relayType, config)
       
@@ -136,7 +212,7 @@ def showWizard():
     # redraws screen to clear away the dialog we just showed
     cli.controller.getController().requestRedraw(True)
 
-def promptRelayType():
+def promptRelayType(initialSelection):
   """
   Provides a prompt for selecting the general role we'd like Tor to run with.
   This returns a RelayType enumeration for the selection, or CANCEL if the
@@ -146,9 +222,9 @@ def promptRelayType():
   popup, _, _ = cli.popups.init(25, 58)
   if not popup: return
   control = cli.controller.getController()
-  key, selection = 0, 0
   options = [ConfigOption(opt, "role", opt) for opt in RelayType.values()]
   options.append(ConfigOption(CANCEL, "general", CANCEL))
+  selection = RelayType.indexOf(initialSelection)
   
   try:
     popup.win.box()
@@ -162,21 +238,21 @@ def promptRelayType():
     while True:
       y, offset = len(topContent) + 1, 0
       
-      for i in range(len(options)):
+      for opt in options:
         optionFormat = uiTools.getColor(MSG_COLOR)
-        if i == selection: optionFormat |= curses.A_STANDOUT
+        if opt == options[selection]: optionFormat |= curses.A_STANDOUT
         
         # Curses has a weird bug where there's a one-pixel alignment
         # difference between bold and regular text, so it looks better
         # to render the whitespace here as not being bold.
         
         offset += 1
-        label = options[i].getLabel(" ")
+        label = opt.getLabel(" ")
         popup.addstr(y + offset, 2, label, optionFormat | curses.A_BOLD)
         popup.addstr(y + offset, 2 + len(label), " " * (54 - len(label)), optionFormat)
         offset += 1
         
-        for line in options[i].getDescription(52, " "):
+        for line in opt.getDescription(52, " "):
           popup.addstr(y + offset, 2, uiTools.padStr(line, 54), optionFormat)
           offset += 1
       
@@ -218,22 +294,42 @@ def promptConfigOptions(relayType, config):
       popup.win.erase()
       popup.win.box()
       
-      # provides the description for internal relays
+      # provides the description for the relay type
       for i in range(len(topContent)):
         popup.addstr(i + 1, 2, topContent[i], curses.A_BOLD | uiTools.getColor(MSG_COLOR))
       
       y, offset = len(topContent) + 1, 0
-      for i in range(len(options)):
-        if options[i] == Options.DIVIDER:
+      for opt in options:
+        if opt == Options.DIVIDER:
           offset += 1
           continue
         
-        label = " %-30s%s" % (options[i].getLabel(), options[i].getDisplayValue())
-        optionFormat = curses.A_BOLD | uiTools.getColor(OPTION_COLOR)
-        if i == selection: optionFormat |= curses.A_STANDOUT
+        optionFormat = opt.getDisplayAttr()
+        if opt == options[selection]: optionFormat |= curses.A_STANDOUT
         
-        offset += 1
-        popup.addstr(y + offset, 2, uiTools.padStr(label, 54), optionFormat)
+        offset, indent = offset + 1, 0
+        if opt.getKey() in CONFIG["wizard.suboptions"]:
+          # If the next entry is also a suboption then show a 'T', otherwise
+          # end the bracketing.
+          
+          bracketChar, nextIndex = curses.ACS_LLCORNER, options.index(opt) + 1
+          if nextIndex < len(options) and isinstance(options[nextIndex], ConfigOption):
+            if options[nextIndex].getKey() in CONFIG["wizard.suboptions"]:
+              bracketChar = curses.ACS_LTEE
+          
+          popup.addch(y + offset, 3, bracketChar, opt.getDisplayAttr())
+          popup.addch(y + offset, 4, curses.ACS_HLINE, opt.getDisplayAttr())
+          
+          indent = 3
+        
+        labelFormat = " %%-%is%%s" % (30 - indent)
+        label = labelFormat % (opt.getLabel(), opt.getDisplayValue())
+        popup.addstr(y + offset, 2 + indent, uiTools.padStr(label, 54 - indent), optionFormat)
+        
+        # little hack to make "Block" policies red
+        if opt != options[selection] and not opt.getValue() and opt.getKey() in CUSTOM_POLICIES:
+          optionFormat = curses.A_BOLD | uiTools.getColor("red")
+          popup.addstr(y + offset, 33, opt.getDisplayValue(), optionFormat)
       
       # divider between the options and description
       offset += 2
@@ -253,8 +349,8 @@ def promptConfigOptions(relayType, config):
         posOffset = -1 if key == curses.KEY_UP else 1
         selection = (selection + posOffset) % len(options)
         
-        # skips dividers
-        while options[selection] == Options.DIVIDER:
+        # skips disabled options and dividers
+        while options[selection] == Options.DIVIDER or not options[selection].isEnabled():
           selection = (selection + posOffset) % len(options)
       elif uiTools.isSelectionKey(key):
         if selection == len(options) - 2: return BACK # selected back
@@ -263,7 +359,9 @@ def promptConfigOptions(relayType, config):
           options[selection].toggle()
         else:
           newValue = popup.getstr(y + selection + 1, 33, options[selection].getValue(), curses.A_STANDOUT | uiTools.getColor(OPTION_COLOR), 23)
-          if newValue: options[selection].setValue(newValue.strip())
+          if newValue:
+            try: options[selection].setValue(newValue.strip())
+            except ValueError, exc: cli.popups.showMsg(str(exc), 3)
       elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel
   finally:
     cli.popups.finalize()
@@ -280,7 +378,16 @@ def _splitStr(msg, width):
   results = []
   while msg:
     msgSegment, msg = uiTools.cropStr(msg, width, None, endType = None, getRemainder = True)
+    if not msgSegment: break # happens if the width is less than the first word
     results.append(msgSegment.strip())
   
   return results
 
+def _exitPolicyAction(config, option, value):
+  """
+  Enables or disables custom exit policy options based on our selection.
+  """
+  
+  for opt in CUSTOM_POLICIES:
+    config[opt].setEnabled(not value)
+
diff --git a/src/settings.cfg b/src/settings.cfg
index 94ff9c1..135b0e2 100644
--- a/src/settings.cfg
+++ b/src/settings.cfg
@@ -343,14 +343,35 @@ msg.ARM_DEBUG Unable to query process resource usage from ps
 # configuration option attributes used in the relay setup wizard
 wizard.message.role Welcome to the Tor network! This will step you through the configuration process for becoming a part of it. To start with, what role would you like to have?
 wizard.message.relay Internal relays provide connections within the Tor network. Since you will only be connecting to Tor users and relays this is an easy, hassle free way of helping to make the network better.
+wizard.message.exit Exits connect between the Tor network and the outside Internet. This is the most vitally important role you can take, but it also needs some forethought. Please read 'http://www.atagar.com/torExitTips/' before proceeding further to avoid any nasty surprises!
 
 wizard.toggle Notify => Yes, No
 wizard.toggle Startup => Yes, No
+wizard.toggle Notice => Yes, No
+wizard.toggle Policy => Default, Custom
+wizard.toggle Websites => Allow, Block
+wizard.toggle Email => Allow, Block
+wizard.toggle Im => Allow, Block
+wizard.toggle Misc => Allow, Block
+wizard.toggle Plaintext => Allow, Block
+
+wizard.suboptions Websites
+wizard.suboptions Email
+wizard.suboptions Im
+wizard.suboptions Misc
+wizard.suboptions Plaintext
 
 wizard.default Nickname => Unnamed
 wizard.default Notify => true
 wizard.default Bandwidth => 5 MB/s
 wizard.default Startup => true
+wizard.default Notice => true
+wizard.default Policy => true
+wizard.default Websites => true
+wizard.default Email => true
+wizard.default Im => true
+wizard.default Misc => true
+wizard.default Plaintext => true
 
 wizard.label.general Cancel => Cancel
 wizard.label.general Back => Previous
@@ -365,10 +386,17 @@ wizard.label.opt Notify => Issue Notification
 wizard.label.opt Bandwidth => Relay Speed
 wizard.label.opt Limit => Monthly Limit
 wizard.label.opt Startup => Run At Startup
+wizard.label.opt Notice => Disclaimer Notice
+wizard.label.opt Policy => Exit Policy
+wizard.label.opt Websites => Web Browsing
+wizard.label.opt Email => Receiving Email
+wizard.label.opt Im => Instant Messaging
+wizard.label.opt Misc => Other Services
+wizard.label.opt Plaintext => Unencrypted Traffic
 
 wizard.description.general Cancel => Close without starting Tor.
 wizard.description.role Relay => Provides interconnections with other Tor relays. This is a safe and easy of making the network better.
-wizard.description.role Exit => Connects between Tor an the outside Internet. This is a vital role, but can lead to abuse complaints.
+wizard.description.role Exit => Connects between Tor network and the outside Internet. This is a vital role, but can lead to abuse complaints.
 wizard.description.role Bridge => Non-public relay specifically for helping censored users.
 wizard.description.role Client => Use the network without contributing to it.
 wizard.description.opt Nickname => Human friendly name for your relay. If this is unique then it's used instead of your fingerprint (a forty character hex string) when pages like TorStatus refer to you.
@@ -377,6 +405,13 @@ wizard.description.opt Notify => Sends automated email notifications to the abov
 wizard.description.opt Bandwidth => Limit for the average rate at which you relay traffic.
 wizard.description.opt Limit => Maximum amount of traffic to relay each month. Some ISPs, like Comcast, cap their customer's Internet usage so this is an easy way of staying below that limit.
 wizard.description.opt Startup => Runs Tor in the background when the system starts.
+wizard.description.opt Notice => Provides a disclaimer that this is an exit on port 80 (http://www.atagar.com/exitNotice).
+wizard.description.opt Policy => Ports allowed to exit from your relay. The default policy allows for common services while limiting the chance of getting a DMCA takedown for torrent traffic (http://www.atagar.com/exitPolicy).
+wizard.description.opt Websites => General Internet browsing including HTTP (80) and HTTPS (443).
+wizard.description.opt Email => Protocols for receiving, but not sending email. This includes POP3 (110), POP3S (995), IMAP (143), and IMAPS (993).
+wizard.description.opt Im => Common instant messaging protocols including Jabber, IRC, ICQ, AIM, Yahoo, MSN, SILC, GroupWise, Gadu-Gadu, Sametime, and Zephyr.
+wizard.description.opt Misc => Protocols from the default policy that aren't among the above.
+wizard.description.opt Plaintext => When blocked the policy will exclude ports that aren't commonly encrypted.
 
 # some config options are fetched via special values
 torrc.map HiddenServiceDir => HiddenServiceOptions





More information about the tor-commits mailing list