home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / arm / cli / wizard.py < prev   
Encoding:
Python Source  |  2012-05-18  |  38.9 KB  |  1,017 lines

  1. """
  2. Provides user prompts for setting up a new relay. This autogenerates a torrc
  3. that's used by arm to run its own tor instance.
  4. """
  5.  
  6. import re
  7. import os
  8. import sys
  9. import random
  10. import shutil
  11. import getpass
  12. import platform
  13. import functools
  14. import curses
  15.  
  16. import cli.popups
  17. import cli.controller
  18.  
  19. from util import connections, enum, log, sysTools, torConfig, torTools, uiTools
  20.  
  21. # template used to generate the torrc
  22. TORRC_TEMPLATE = "resources/torrcTemplate.txt"
  23.  
  24. # basic configuration types we can run as
  25. RelayType = enum.Enum("RESUME", "RELAY", "EXIT", "BRIDGE", "CLIENT")
  26.  
  27. # all options that can be configured
  28. Options = enum.Enum("DIVIDER", "CONTROL", "NICKNAME", "CONTACT", "NOTIFY", "BANDWIDTH", "LIMIT", "CLIENT", "LOWPORTS", "PORTFORWARD", "SYSTEM", "STARTUP", "RSHUTDOWN", "CSHUTDOWN", "NOTICE", "POLICY", "WEBSITES", "EMAIL", "IM", "MISC", "PLAINTEXT", "DISTRIBUTE", "BRIDGED", "BRIDGE1", "BRIDGE2", "BRIDGE3", "REUSE")
  29. RelayOptions = {RelayType.RELAY:   (Options.NICKNAME,
  30.                                     Options.CONTACT,
  31.                                     Options.NOTIFY,
  32.                                     Options.BANDWIDTH,
  33.                                     Options.LIMIT,
  34.                                     Options.CLIENT,
  35.                                     Options.LOWPORTS,
  36.                                     Options.PORTFORWARD,
  37.                                     Options.STARTUP,
  38.                                     Options.RSHUTDOWN,
  39.                                     Options.SYSTEM),
  40.                 RelayType.EXIT:    (Options.NICKNAME,
  41.                                     Options.CONTACT,
  42.                                     Options.NOTIFY,
  43.                                     Options.BANDWIDTH,
  44.                                     Options.LIMIT,
  45.                                     Options.CLIENT,
  46.                                     Options.LOWPORTS,
  47.                                     Options.PORTFORWARD,
  48.                                     Options.STARTUP,
  49.                                     Options.RSHUTDOWN,
  50.                                     Options.SYSTEM,
  51.                                     Options.DIVIDER,
  52.                                     Options.NOTICE,
  53.                                     Options.POLICY,
  54.                                     Options.WEBSITES,
  55.                                     Options.EMAIL,
  56.                                     Options.IM,
  57.                                     Options.MISC,
  58.                                     Options.PLAINTEXT),
  59.                 RelayType.BRIDGE:  (Options.DISTRIBUTE,
  60.                                     Options.BANDWIDTH,
  61.                                     Options.LIMIT,
  62.                                     Options.CLIENT,
  63.                                     Options.LOWPORTS,
  64.                                     Options.PORTFORWARD,
  65.                                     Options.STARTUP,
  66.                                     Options.RSHUTDOWN,
  67.                                     Options.SYSTEM),
  68.                 RelayType.CLIENT:  (Options.BRIDGED,
  69.                                     Options.BRIDGE1,
  70.                                     Options.BRIDGE2,
  71.                                     Options.BRIDGE3,
  72.                                     Options.REUSE,
  73.                                     Options.CSHUTDOWN,
  74.                                     Options.SYSTEM)}
  75.  
  76. # option sets
  77. CUSTOM_POLICIES = (Options.WEBSITES, Options.EMAIL, Options.IM, Options.MISC, Options.PLAINTEXT)
  78. BRIDGE_ENTRIES = (Options.BRIDGE1, Options.BRIDGE2, Options.BRIDGE3)
  79.  
  80. # other options provided in the prompts
  81. CANCEL, NEXT, BACK = "Cancel", "Next", "Back"
  82.  
  83. DESC_SIZE = 5 # height of the description field
  84. MSG_COLOR = "green"
  85. OPTION_COLOR = "yellow"
  86. DISABLED_COLOR = "cyan"
  87.  
  88. # bracketing pairs used in email address obscuring
  89. BRACKETS = ((' ', ' '),
  90.             ('<', '>'),
  91.             ('[', ']'),
  92.             ('(', ')'),
  93.             ('{', '}'),
  94.             ('|', '|'))
  95.  
  96. # version requirements for options
  97. VERSION_REQUIREMENTS = {Options.PORTFORWARD: "0.2.3.1-alpha"}
  98.  
  99. # tor's defaults for config options, used to filter unneeded options
  100. TOR_DEFAULTS = {Options.BANDWIDTH: "5 MB",
  101.                 Options.REUSE: "10 minutes"}
  102.  
  103. # path for the torrc to be placed if replacing the torrc for the system wide
  104. # tor instance
  105. SYSTEM_DROP_PATH = "/var/lib/tor-arm/torrc"
  106. OVERRIDE_SCRIPT = "/usr/share/arm/resources/torrcOverride/override.py"
  107. OVERRIDE_SETUID_SCRIPT = "/usr/bin/torrc-override"
  108.  
  109. CONFIG = {"wizard.message.role": "",
  110.           "wizard.message.relay": "",
  111.           "wizard.message.exit": "",
  112.           "wizard.message.bridge": "",
  113.           "wizard.message.client": "",
  114.           "wizard.toggle": {},
  115.           "wizard.disabled": [],
  116.           "wizard.suboptions": [],
  117.           "wizard.default": {},
  118.           "wizard.blankValue": {},
  119.           "wizard.label.general": {},
  120.           "wizard.label.role": {},
  121.           "wizard.label.opt": {},
  122.           "wizard.description.general": {},
  123.           "wizard.description.role": {},
  124.           "wizard.description.opt": {},
  125.           "port.category": {},
  126.           "port.exit.all": [],
  127.           "port.exit.web": [],
  128.           "port.exit.mail": [],
  129.           "port.exit.im": [],
  130.           "port.exit.misc": [],
  131.           "port.encrypted": []}
  132.  
  133. def loadConfig(config):
  134.   config.update(CONFIG)
  135.  
  136. class ConfigOption:
  137.   """
  138.   Attributes of a configuraition option.
  139.   """
  140.   
  141.   def __init__(self, key, group, default):
  142.     """
  143.     Configuration option constructor.
  144.     
  145.     Arguments:
  146.       key     - configuration option identifier used when querying attributes
  147.       group   - configuration attribute group this belongs to
  148.       default - initial value, uses the config default if unset
  149.     """
  150.     
  151.     self.key = key
  152.     self.group = group
  153.     self.descriptionCache = None
  154.     self.descriptionCacheArg = None
  155.     self.value = default
  156.     self.validator = None
  157.     self._isEnabled = True
  158.   
  159.   def getKey(self):
  160.     return self.key
  161.   
  162.   def getValue(self):
  163.     return self.value
  164.   
  165.   def getDisplayValue(self):
  166.     if not self.value and self.key in CONFIG["wizard.blankValue"]:
  167.       return CONFIG["wizard.blankValue"][self.key]
  168.     else: return self.value
  169.   
  170.   def getDisplayAttr(self):
  171.     myColor = OPTION_COLOR if self.isEnabled() else DISABLED_COLOR
  172.     return curses.A_BOLD | uiTools.getColor(myColor)
  173.   
  174.   def isEnabled(self):
  175.     return self._isEnabled
  176.   
  177.   def setEnabled(self, isEnabled):
  178.     self._isEnabled = isEnabled
  179.   
  180.   def setValidator(self, validator):
  181.     """
  182.     Custom function used to check that a value is valid before setting it.
  183.     This functor should accept two arguments: this option and the value we're
  184.     attempting to set. If its invalid then a ValueError with the reason is
  185.     expected.
  186.     
  187.     Arguments:
  188.       validator - functor for checking the validitiy of values we set
  189.     """
  190.     
  191.     self.validator = validator
  192.   
  193.   def setValue(self, value):
  194.     """
  195.     Attempts to set our value. If a validator has been set then we first check
  196.     if it's alright, raising a ValueError with the reason if not.
  197.     
  198.     Arguments:
  199.       value - value we're attempting to set
  200.     """
  201.     
  202.     if self.validator: self.validator(self, value)
  203.     self.value = value
  204.   
  205.   def getLabel(self, prefix = ""):
  206.     return prefix + CONFIG["wizard.label.%s" % self.group].get(self.key, "")
  207.   
  208.   def getDescription(self, width, prefix = ""):
  209.     if not self.descriptionCache or self.descriptionCacheArg != width:
  210.       optDescription = CONFIG["wizard.description.%s" % self.group].get(self.key, "")
  211.       self.descriptionCache = _splitStr(optDescription, width)
  212.       self.descriptionCacheArg = width
  213.     
  214.     return [prefix + line for line in self.descriptionCache]
  215.  
  216. class ToggleConfigOption(ConfigOption):
  217.   """
  218.   Configuration option representing a boolean.
  219.   """
  220.   
  221.   def __init__(self, key, group, default, trueLabel, falseLabel):
  222.     ConfigOption.__init__(self, key, group, default)
  223.     self.trueLabel = trueLabel
  224.     self.falseLabel = falseLabel
  225.   
  226.   def getDisplayValue(self):
  227.     return self.trueLabel if self.value else self.falseLabel
  228.   
  229.   def toggle(self):
  230.     # This isn't really here to validate the value (after all this is a
  231.     # boolean, the options are limited!), but rather give a method for functors
  232.     # to be triggered when selected.
  233.     
  234.     if self.validator: self.validator(self, not self.value)
  235.     self.value = not self.value
  236.  
  237. def showWizard():
  238.   """
  239.   Provides a series of prompts, allowing the user to spawn a customized tor
  240.   instance.
  241.   """
  242.   
  243.   if not sysTools.isAvailable("tor"):
  244.     msg = "Unable to run the setup wizard. Is tor installed?"
  245.     log.log(log.WARN, msg)
  246.     return
  247.   
  248.   # gets tor's version
  249.   torVersion = None
  250.   try:
  251.     versionQuery = sysTools.call("tor --version")
  252.     
  253.     for line in versionQuery:
  254.       if line.startswith("Tor version "):
  255.         torVersion = torTools.parseVersion(line.split(" ")[2])
  256.         break
  257.   except IOError, exc:
  258.     log.log(log.INFO, "'tor --version' query failed: %s" % exc)
  259.   
  260.   relayType, config = None, {}
  261.   for option in Options.values():
  262.     if option == Options.DIVIDER:
  263.       config[option] = option
  264.       continue
  265.     
  266.     toggleValues = CONFIG["wizard.toggle"].get(option)
  267.     default = CONFIG["wizard.default"].get(option, "")
  268.     
  269.     if toggleValues:
  270.       if "," in toggleValues:
  271.         trueLabel, falseLabel = toggleValues.split(",", 1)
  272.       else: trueLabel, falseLabel = toggleValues, ""
  273.       
  274.       isSet = default.lower() == "true"
  275.       config[option] = ToggleConfigOption(option, "opt", isSet, trueLabel.strip(), falseLabel.strip())
  276.     else: config[option] = ConfigOption(option, "opt", default)
  277.   
  278.   # sets input validators
  279.   config[Options.BANDWIDTH].setValidator(_relayRateValidator)
  280.   config[Options.LIMIT].setValidator(_monthlyLimitValidator)
  281.   config[Options.BRIDGE1].setValidator(_bridgeDestinationValidator)
  282.   config[Options.BRIDGE2].setValidator(_bridgeDestinationValidator)
  283.   config[Options.BRIDGE3].setValidator(_bridgeDestinationValidator)
  284.   config[Options.REUSE].setValidator(_circDurationValidator)
  285.   
  286.   # enables custom policies when 'custom' is selected and disables otherwise
  287.   policyOpt = config[Options.POLICY]
  288.   customPolicies = [config[opt] for opt in CUSTOM_POLICIES]
  289.   policyOpt.setValidator(functools.partial(_toggleEnabledAction, customPolicies))
  290.   _toggleEnabledAction(customPolicies, policyOpt, policyOpt.getValue())
  291.   
  292.   lowPortsOpt = config[Options.LOWPORTS]
  293.   disclaimerNotice = [config[Options.NOTICE]]
  294.   lowPortsOpt.setValidator(functools.partial(_toggleEnabledAction, disclaimerNotice))
  295.   _toggleEnabledAction(disclaimerNotice, lowPortsOpt, lowPortsOpt.getValue())
  296.   
  297.   # enables bridge entries when "Use Bridges" is set and disables otherwise
  298.   useBridgeOpt = config[Options.BRIDGED]
  299.   bridgeEntries = [config[opt] for opt in BRIDGE_ENTRIES]
  300.   useBridgeOpt.setValidator(functools.partial(_toggleEnabledAction, bridgeEntries))
  301.   _toggleEnabledAction(bridgeEntries, useBridgeOpt, useBridgeOpt.getValue())
  302.   
  303.   # enables running at startup when 'Use System Instance' is deselected and
  304.   # disables otherwise
  305.   systemOpt = config[Options.SYSTEM]
  306.   startupOpt = [config[Options.STARTUP]]
  307.   systemOpt.setValidator(functools.partial(_toggleEnabledAction, startupOpt, True))
  308.   _toggleEnabledAction(startupOpt, systemOpt, not systemOpt.getValue())
  309.   
  310.   # remembers the last selection made on the type prompt page
  311.   controller = cli.controller.getController()
  312.   manager = controller.getTorManager()
  313.   relaySelection = RelayType.RESUME if manager.isTorrcAvailable() else RelayType.RELAY
  314.   
  315.   # excludes options that are either disabled or for a future tor version
  316.   disabledOpt = list(CONFIG["wizard.disabled"])
  317.   
  318.   for opt, optVersion in VERSION_REQUIREMENTS.items():
  319.     if not torVersion or not torTools.isVersion(torVersion, torTools.parseVersion(optVersion)):
  320.       disabledOpt.append(opt)
  321.   
  322.   # the port forwarding option would only work if tor-fw-helper is in the path
  323.   if not Options.PORTFORWARD in disabledOpt:
  324.     if not sysTools.isAvailable("tor-fw-helper"):
  325.       disabledOpt.append(Options.PORTFORWARD)
  326.   
  327.   # If we haven't run 'resources/torrcOverride/override.py --init' or lack
  328.   # permissions then we aren't able to deal with the system wide tor instance.
  329.   # Also drop the option if we aren't installed since override.py won't be at
  330.   # the expected path.
  331.   if not os.path.exists(os.path.dirname(SYSTEM_DROP_PATH)) or not os.path.exists(OVERRIDE_SCRIPT):
  332.     disabledOpt.append(Options.SYSTEM)
  333.   
  334.   # TODO: The STARTUP option is currently disabled in the 'settings.cfg', and I
  335.   # don't currently have plans to implement it (it would be a big pita, and the
  336.   # tor deb already handles it). *If* it is implemented then I'd limit support
  337.   # for the option to Debian and Ubuntu to start with, via the following...
  338.   
  339.   # Running at startup is currently only supported for Debian and Ubuntu.
  340.   # Patches welcome for supporting other platforms.
  341.   #if not platform.dist()[0] in ("debian", "Ubuntu"):
  342.   #  disabledOpt.append(Options.STARTUP)
  343.   
  344.   while True:
  345.     if relayType == None:
  346.       selection = promptRelayType(relaySelection)
  347.       
  348.       if selection == CANCEL: break
  349.       elif selection == RelayType.RESUME:
  350.         if not manager.isManaged(torTools.getConn()):
  351.           manager.startManagedInstance()
  352.         
  353.         break
  354.       else: relayType, relaySelection = selection, selection
  355.     else:
  356.       selection = promptConfigOptions(relayType, config, disabledOpt)
  357.       
  358.       if selection == BACK: relayType = None
  359.       elif selection == CANCEL: break
  360.       elif selection == NEXT:
  361.         generatedTorrc = getTorrc(relayType, config, disabledOpt)
  362.         
  363.         torrcLocation = manager.getTorrcPath()
  364.         isSystemReplace = not Options.SYSTEM in disabledOpt and config[Options.SYSTEM].getValue()
  365.         if isSystemReplace: torrcLocation = SYSTEM_DROP_PATH
  366.         
  367.         controller.redraw()
  368.         confirmationSelection = showConfirmationDialog(generatedTorrc, torrcLocation)
  369.         
  370.         if confirmationSelection == NEXT:
  371.           log.log(log.INFO, "Writing torrc to '%s':\n%s" % (torrcLocation, generatedTorrc))
  372.           
  373.           # if the torrc already exists then save it to a _bak file
  374.           isBackedUp = False
  375.           if os.path.exists(torrcLocation) and not isSystemReplace:
  376.             try:
  377.               shutil.copy(torrcLocation, torrcLocation + "_bak")
  378.               isBackedUp = True
  379.             except IOError, exc:
  380.               log.log(log.WARN, "Unable to backup the torrc: %s" % exc)
  381.           
  382.           # writes the torrc contents
  383.           try:
  384.             torrcFile = open(torrcLocation, "w")
  385.             torrcFile.write(generatedTorrc)
  386.             torrcFile.close()
  387.           except IOError, exc:
  388.             log.log(log.ERR, "Unable to make torrc: %s" % exc)
  389.             break
  390.           
  391.           # logs where we placed the torrc
  392.           msg = "Tor configuration placed at '%s'" % torrcLocation
  393.           if isBackedUp:
  394.             msg += " (the previous torrc was moved to 'torrc_bak')"
  395.           
  396.           log.log(log.NOTICE, msg)
  397.           
  398.           dataDir = cli.controller.getController().getDataDirectory()
  399.           
  400.           pathPrefix = os.path.dirname(sys.argv[0])
  401.           if pathPrefix and not pathPrefix.endswith("/"):
  402.             pathPrefix = pathPrefix + "/"
  403.           
  404.           # copies exit notice into data directory if it's being used
  405.           if Options.NOTICE in RelayOptions[relayType] and config[Options.NOTICE].getValue() and config[Options.LOWPORTS].getValue():
  406.             src = "%sresources/exitNotice" % pathPrefix
  407.             dst = "%sexitNotice" % dataDir
  408.             
  409.             if not os.path.exists(dst):
  410.               shutil.copytree(src, dst)
  411.             
  412.             # providing a notice that it has sections specific to us operators
  413.             msg = "Exit notice placed at '%s/index.html'. Some of the sections are specific to US relay operators so please change the \"FIXME\" sections if this is inappropriate." % dst
  414.             log.log(log.NOTICE, msg)
  415.           
  416.           runCommand, exitCode = None, 1
  417.           
  418.           if isSystemReplace:
  419.             # running override.py needs root so...
  420.             # - if running as root (bad user, no biscuit!) then run it directly
  421.             # - if the setuid binary is available at '/usr/bin/torrc-override'
  422.             #   then use that
  423.             # - attempt sudo in case passwordless sudo is available
  424.             # - if all of the above fail then log instructions
  425.             
  426.             if os.geteuid() == 0: runCommand = OVERRIDE_SCRIPT
  427.             elif os.path.exists(OVERRIDE_SETUID_SCRIPT): runCommand = OVERRIDE_SETUID_SCRIPT
  428.             else:
  429.               # The -n argument to sudo is *supposed* to be available starting
  430.               # with 1.7.0 [1] however this is a dirty lie (Ubuntu 9.10 uses
  431.               # 1.7.0 and even has the option in its man page, but it doesn't
  432.               # work). Instead checking for version 1.7.1.
  433.               #
  434.               # [1] http://www.sudo.ws/pipermail/sudo-users/2009-January/003889.html
  435.               
  436.               sudoVersionResult = sysTools.call("sudo -V")
  437.               
  438.               # version output looks like "Sudo version 1.7.2p7"
  439.               if len(sudoVersionResult) == 1 and sudoVersionResult[0].count(" ") >= 2:
  440.                 versionNum = 0
  441.                 
  442.                 for comp in sudoVersionResult[0].split(" ")[2].split("."):
  443.                   if comp and comp[0].isdigit():
  444.                     versionNum = (10 * versionNum) + int(comp)
  445.                   else:
  446.                     # invalid format
  447.                     log.log(log.INFO, "Unrecognized sudo version string: %s" % sudoVersionResult[0])
  448.                     versionNum = 0
  449.                     break
  450.                 
  451.                 if versionNum >= 171:
  452.                   runCommand = "sudo -n %s" % OVERRIDE_SCRIPT
  453.                 else:
  454.                   log.log(log.INFO, "Insufficient sudo version for the -n argument")
  455.             
  456.             if runCommand: exitCode = os.system("%s > /dev/null 2>&1" % runCommand)
  457.             
  458.             if exitCode != 0:
  459.               msg = "Tor needs root permissions to replace the system wide torrc. To continue...\n- open another terminal\n- run \"sudo %s\"\n- press 'x' here to tell tor to reload" % OVERRIDE_SCRIPT
  460.               log.log(log.NOTICE, msg)
  461.             else: torTools.getConn().reload()
  462.           elif manager.isTorrcAvailable():
  463.             # If we're connected to a managed instance then just need to
  464.             # issue a sighup to pick up the new settings. Otherwise starts
  465.             # a new tor instance.
  466.             
  467.             conn = torTools.getConn()
  468.             if manager.isManaged(conn): conn.reload()
  469.             else: manager.startManagedInstance()
  470.           else:
  471.             # If we don't have permissions to run the torrc we just made then
  472.             # makes a shell script they can run as root to start tor.
  473.             
  474.             src = "%sresources/startTor" % pathPrefix
  475.             dst = "%sstartTor" % dataDir
  476.             if not os.path.exists(dst): shutil.copy(src, dst)
  477.             
  478.             msg = "Tor needs root permissions to start with this configuration (it will drop itself to the current user afterward). To continue...\n- open another terminal\n- run \"sudo %s\"\n- press 'r' here to tell arm to reconnect" % dst
  479.             log.log(log.NOTICE, msg)
  480.           
  481.           break
  482.         elif confirmationSelection == CANCEL: break
  483.     
  484.     # redraws screen to clear away the dialog we just showed
  485.     cli.controller.getController().redraw()
  486.  
  487. def promptRelayType(initialSelection):
  488.   """
  489.   Provides a prompt for selecting the general role we'd like Tor to run with.
  490.   This returns a RelayType enumeration for the selection, or CANCEL if the
  491.   dialog was canceled.
  492.   """
  493.   
  494.   options = [ConfigOption(opt, "role", opt) for opt in RelayType.values()]
  495.   options.append(ConfigOption(CANCEL, "general", CANCEL))
  496.   selection = RelayType.indexOf(initialSelection)
  497.   height = 28
  498.   
  499.   # drops the resume option if it isn't applicable
  500.   control = cli.controller.getController()
  501.   if not control.getTorManager().isTorrcAvailable():
  502.     options.pop(0)
  503.     height -= 3
  504.     selection -= 1
  505.   
  506.   popup, _, _ = cli.popups.init(height, 58)
  507.   if not popup: return
  508.   
  509.   try:
  510.     popup.win.box()
  511.     curses.cbreak()
  512.     
  513.     # provides the welcoming message
  514.     topContent = _splitStr(CONFIG["wizard.message.role"], 54)
  515.     for i in range(len(topContent)):
  516.       popup.addstr(i + 1, 2, topContent[i], curses.A_BOLD | uiTools.getColor(MSG_COLOR))
  517.     
  518.     while True:
  519.       y, offset = len(topContent) + 1, 0
  520.       
  521.       for opt in options:
  522.         optionFormat = uiTools.getColor(MSG_COLOR)
  523.         if opt == options[selection]: optionFormat |= curses.A_STANDOUT
  524.         
  525.         # Curses has a weird bug where there's a one-pixel alignment
  526.         # difference between bold and regular text, so it looks better
  527.         # to render the whitespace here as not being bold.
  528.         
  529.         offset += 1
  530.         label = opt.getLabel(" ")
  531.         popup.addstr(y + offset, 2, label, optionFormat | curses.A_BOLD)
  532.         popup.addstr(y + offset, 2 + len(label), " " * (54 - len(label)), optionFormat)
  533.         offset += 1
  534.         
  535.         for line in opt.getDescription(52, " "):
  536.           popup.addstr(y + offset, 2, uiTools.padStr(line, 54), optionFormat)
  537.           offset += 1
  538.       
  539.       popup.win.refresh()
  540.       key = control.getScreen().getch()
  541.       
  542.       if key == curses.KEY_UP: selection = (selection - 1) % len(options)
  543.       elif key == curses.KEY_DOWN: selection = (selection + 1) % len(options)
  544.       elif uiTools.isSelectionKey(key): return options[selection].getValue()
  545.       elif key in (27, ord('q'), ord('Q')): return CANCEL # esc or q - cancel
  546.   finally:
  547.     cli.popups.finalize()
  548.  
  549. def promptConfigOptions(relayType, config, disabledOpt):
  550.   """
  551.   Prompts the user for the configuration of an internal relay.
  552.   """
  553.   
  554.   topContent = _splitStr(CONFIG.get("wizard.message.%s" % relayType.lower(), ""), 54)
  555.   
  556.   options = [config[opt] for opt in RelayOptions[relayType] if not opt in disabledOpt]
  557.   options.append(Options.DIVIDER)
  558.   options.append(ConfigOption(BACK, "general", "(to role selection)"))
  559.   options.append(ConfigOption(NEXT, "general", "(to confirm options)"))
  560.   
  561.   popupHeight = len(topContent) + len(options) + DESC_SIZE + 5
  562.   popup, _, _ = cli.popups.init(popupHeight, 58)
  563.   if not popup: return
  564.   control = cli.controller.getController()
  565.   key, selection = 0, 0
  566.   
  567.   try:
  568.     curses.cbreak()
  569.     
  570.     while True:
  571.       popup.win.erase()
  572.       popup.win.box()
  573.       
  574.       # provides the description for the relay type
  575.       for i in range(len(topContent)):
  576.         popup.addstr(i + 1, 2, topContent[i], curses.A_BOLD | uiTools.getColor(MSG_COLOR))
  577.       
  578.       y, offset = len(topContent) + 1, 0
  579.       for opt in options:
  580.         if opt == Options.DIVIDER:
  581.           offset += 1
  582.           continue
  583.         
  584.         optionFormat = opt.getDisplayAttr()
  585.         if opt == options[selection]: optionFormat |= curses.A_STANDOUT
  586.         
  587.         offset, indent = offset + 1, 0
  588.         if opt.getKey() in CONFIG["wizard.suboptions"]:
  589.           # If the next entry is also a suboption then show a 'T', otherwise
  590.           # end the bracketing.
  591.           
  592.           bracketChar, nextIndex = curses.ACS_LLCORNER, options.index(opt) + 1
  593.           if nextIndex < len(options) and isinstance(options[nextIndex], ConfigOption):
  594.             if options[nextIndex].getKey() in CONFIG["wizard.suboptions"]:
  595.               bracketChar = curses.ACS_LTEE
  596.           
  597.           popup.addch(y + offset, 3, bracketChar, opt.getDisplayAttr())
  598.           popup.addch(y + offset, 4, curses.ACS_HLINE, opt.getDisplayAttr())
  599.           
  600.           indent = 3
  601.         
  602.         labelFormat = " %%-%is%%s" % (30 - indent)
  603.         label = labelFormat % (opt.getLabel(), opt.getDisplayValue())
  604.         popup.addstr(y + offset, 2 + indent, uiTools.padStr(label, 54 - indent), optionFormat)
  605.         
  606.         # little hack to make "Block" policies red
  607.         if opt != options[selection] and not opt.getValue() and opt.getKey() in CUSTOM_POLICIES:
  608.           optionFormat = curses.A_BOLD | uiTools.getColor("red")
  609.           popup.addstr(y + offset, 33, opt.getDisplayValue(), optionFormat)
  610.       
  611.       # divider between the options and description
  612.       offset += 2
  613.       popup.addch(y + offset, 0, curses.ACS_LTEE)
  614.       popup.addch(y + offset, popup.getWidth() - 1, curses.ACS_RTEE)
  615.       popup.hline(y + offset, 1, popup.getWidth() - 2)
  616.       
  617.       # description for the currently selected option
  618.       for line in options[selection].getDescription(54, " "):
  619.         offset += 1
  620.         popup.addstr(y + offset, 1, line, uiTools.getColor(MSG_COLOR))
  621.       
  622.       popup.win.refresh()
  623.       key = control.getScreen().getch()
  624.       
  625.       if key in (curses.KEY_UP, curses.KEY_DOWN):
  626.         posOffset = -1 if key == curses.KEY_UP else 1
  627.         selection = (selection + posOffset) % len(options)
  628.         
  629.         # skips disabled options and dividers
  630.         while options[selection] == Options.DIVIDER or not options[selection].isEnabled():
  631.           selection = (selection + posOffset) % len(options)
  632.       elif uiTools.isSelectionKey(key):
  633.         if selection == len(options) - 2: return BACK # selected back
  634.         elif selection == len(options) - 1: return NEXT # selected next
  635.         elif isinstance(options[selection], ToggleConfigOption):
  636.           options[selection].toggle()
  637.         else:
  638.           newValue = popup.getstr(y + selection + 1, 33, options[selection].getValue(), curses.A_STANDOUT | uiTools.getColor(OPTION_COLOR), 23)
  639.           if newValue:
  640.             try: options[selection].setValue(newValue.strip())
  641.             except ValueError, exc:
  642.               cli.popups.showMsg(str(exc), 3)
  643.               cli.controller.getController().redraw()
  644.       elif key in (27, ord('q'), ord('Q')): return CANCEL
  645.   finally:
  646.     cli.popups.finalize()
  647.  
  648. def getTorrc(relayType, config, disabledOpt):
  649.   """
  650.   Provides the torrc generated for the given options.
  651.   """
  652.   
  653.   # TODO: When Robert's 'ownership' feature is available take advantage of it
  654.   # for the RSHUTDOWN and CSHUTDOWN options.
  655.   
  656.   pathPrefix = os.path.dirname(sys.argv[0])
  657.   if pathPrefix and not pathPrefix.endswith("/"):
  658.     pathPrefix = pathPrefix + "/"
  659.   
  660.   templateFile = open("%s%s" % (pathPrefix, TORRC_TEMPLATE), "r")
  661.   template = templateFile.readlines()
  662.   templateFile.close()
  663.   
  664.   # generates the options the template expects
  665.   templateOptions = {}
  666.   
  667.   for key, value in config.items():
  668.     if isinstance(value, ConfigOption):
  669.       value = value.getValue()
  670.     
  671.     if key == Options.BANDWIDTH and value.endswith("/s"):
  672.       # truncates "/s" from the rate for RelayBandwidthRate entry
  673.       value = value[:-2]
  674.     elif key == Options.NOTICE:
  675.       # notice option is only applied if using low ports
  676.       value &= config[Options.LOWPORTS].getValue()
  677.     elif key == Options.CONTACT and _isEmailAddress(value):
  678.       # obscures the email address
  679.       value = _obscureEmailAddress(value)
  680.     
  681.     templateOptions[key.upper()] = value
  682.   
  683.   templateOptions[relayType.upper()] = True
  684.   templateOptions["LOW_PORTS"] = config[Options.LOWPORTS].getValue()
  685.   
  686.   # uses double the relay rate for bursts
  687.   bwOpt = Options.BANDWIDTH.upper()
  688.   
  689.   if templateOptions[bwOpt] != TOR_DEFAULTS[Options.BANDWIDTH]:
  690.     relayRateComp = templateOptions[bwOpt].split(" ")
  691.     templateOptions["BURST"] = "%i %s" % (int(relayRateComp[0]) * 2, " ".join(relayRateComp[1:]))
  692.   
  693.   # paths for our tor related resources
  694.   
  695.   dataDir = cli.controller.getController().getDataDirectory()
  696.   templateOptions["NOTICE_PATH"] = "%sexitNotice/index.html" % dataDir
  697.   templateOptions["LOG_ENTRY"] = "notice file %stor_log" % dataDir
  698.   templateOptions["USERNAME"] = getpass.getuser()
  699.   
  700.   # using custom data directory, unless this is for a system wide instance
  701.   if not config[Options.SYSTEM].getValue() or Options.SYSTEM in disabledOpt:
  702.     templateOptions["DATA_DIR"] = "%stor_data" % dataDir
  703.   
  704.   policyCategories = []
  705.   if not config[Options.POLICY].getValue():
  706.     policyCategories = ["web", "mail", "im", "misc"]
  707.   else:
  708.     if config[Options.WEBSITES].getValue(): policyCategories.append("web")
  709.     if config[Options.EMAIL].getValue(): policyCategories.append("mail")
  710.     if config[Options.IM].getValue(): policyCategories.append("im")
  711.     if config[Options.MISC].getValue(): policyCategories.append("misc")
  712.   
  713.   # uses the CSHUTDOWN or RSHUTDOWN option based on if we're running as a
  714.   # client or not
  715.   if relayType == RelayType.CLIENT:
  716.     templateOptions["SHUTDOWN"] = templateOptions[Options.CSHUTDOWN.upper()]
  717.   else:
  718.     templateOptions["SHUTDOWN"] = templateOptions[Options.RSHUTDOWN.upper()]
  719.   
  720.   if policyCategories:
  721.     isEncryptedOnly = not config[Options.PLAINTEXT].getValue()
  722.     
  723.     policyLines = []
  724.     for category in ["all"] + policyCategories:
  725.       # shows a comment at the start of the section saying what it's for
  726.       topicComment = CONFIG["port.category"].get(category)
  727.       if topicComment:
  728.         for topicComp in _splitStr(topicComment, 78):
  729.           policyLines.append("# " + topicComp)
  730.       
  731.       for portEntry in CONFIG.get("port.exit.%s" % category, []):
  732.         # port entry might be an individual port or a range
  733.         
  734.         if isEncryptedOnly and (not portEntry in CONFIG["port.encrypted"]):
  735.           continue # opting to not include plaintext port and ranges
  736.         
  737.         if "-" in portEntry:
  738.           # if this is a range then use the first port's description
  739.           comment = connections.PORT_USAGE.get(portEntry[:portEntry.find("-")])
  740.         else: comment = connections.PORT_USAGE.get(portEntry)
  741.         
  742.         entry = "ExitPolicy accept *:%s" % portEntry
  743.         if comment: policyLines.append("%-30s# %s" % (entry, comment))
  744.         else: policyLines.append(entry)
  745.       
  746.       if category != policyCategories[-1]:
  747.         policyLines.append("") # newline to split categories
  748.     
  749.     templateOptions["EXIT_POLICY"] = "\n".join(policyLines)
  750.   
  751.   # includes input bridges
  752.   bridgeLines = []
  753.   for bridgeOpt in [Options.BRIDGE1, Options.BRIDGE2, Options.BRIDGE3]:
  754.     bridgeValue = config[bridgeOpt].getValue()
  755.     if bridgeValue: bridgeLines.append("Bridge %s" % bridgeValue)
  756.   
  757.   templateOptions["BRIDGES"] = "\n".join(bridgeLines)
  758.   
  759.   # removes disabled options
  760.   for opt in disabledOpt:
  761.     if opt.upper() in templateOptions:
  762.       del templateOptions[opt.upper()]
  763.   
  764.   startupOpt = Options.STARTUP.upper()
  765.   if not config[Options.STARTUP].isEnabled() and startupOpt in templateOptions:
  766.     del templateOptions[startupOpt]
  767.   
  768.   # removes options if they match the tor defaults
  769.   for opt in TOR_DEFAULTS:
  770.     if templateOptions[opt.upper()] == TOR_DEFAULTS[opt]:
  771.       del templateOptions[opt.upper()]
  772.   
  773.   return torConfig.renderTorrc(template, templateOptions)
  774.  
  775. def showConfirmationDialog(torrcContents, torrcLocation):
  776.   """
  777.   Shows a confirmation dialog with the given torrc contents, returning CANCEL,
  778.   NEXT, or BACK based on the selection.
  779.   
  780.   Arguments:
  781.     torrcContents - lines of torrc contents to be presented
  782.     torrcLocation - path where the torrc will be placed
  783.   """
  784.   
  785.   torrcLines = torrcContents.split("\n")
  786.   options = ["Cancel", "Back to Setup", "Start Tor"]
  787.   
  788.   control = cli.controller.getController()
  789.   screenHeight = control.getScreen().getmaxyx()[0]
  790.   stickyHeight = sum([stickyPanel.getHeight() for stickyPanel in control.getStickyPanels()])
  791.   isScrollbarVisible = len(torrcLines) + stickyHeight + 5 > screenHeight
  792.   
  793.   xOffset = 3 if isScrollbarVisible else 0
  794.   popup, width, height = cli.popups.init(len(torrcLines) + 5, 84 + xOffset)
  795.   if not popup: return False
  796.   
  797.   try:
  798.     scroll, selection = 0, 2
  799.     curses.cbreak()
  800.     
  801.     while True:
  802.       popup.win.erase()
  803.       popup.win.box()
  804.       
  805.       # renders the scrollbar
  806.       if isScrollbarVisible:
  807.         popup.addScrollBar(scroll, scroll + height - 5, len(torrcLines), 1, height - 4, 1)
  808.       
  809.       # shows the path where the torrc will be placed
  810.       titleMsg = "The following will be placed at '%s':" % torrcLocation
  811.       popup.addstr(0, 0, titleMsg, curses.A_STANDOUT)
  812.       
  813.       # renders the torrc contents
  814.       for i in range(scroll, min(len(torrcLines), height - 5 + scroll)):
  815.         # parses the argument and comment from options
  816.         option, arg, comment = uiTools.cropStr(torrcLines[i], width - 4 - xOffset), "", ""
  817.         
  818.         div = option.find("#")
  819.         if div != -1: option, comment = option[:div], option[div:]
  820.         
  821.         div = option.strip().find(" ")
  822.         if div != -1: option, arg = option[:div], option[div:]
  823.         
  824.         drawX = 2 + xOffset
  825.         popup.addstr(i + 1 - scroll, drawX, option, curses.A_BOLD | uiTools.getColor("green"))
  826.         drawX += len(option)
  827.         popup.addstr(i + 1 - scroll, drawX, arg, curses.A_BOLD | uiTools.getColor("cyan"))
  828.         drawX += len(arg)
  829.         popup.addstr(i + 1 - scroll, drawX, comment, uiTools.getColor("white"))
  830.       
  831.       # divider between the torrc and the options
  832.       popup.addch(height - 4, 0, curses.ACS_LTEE)
  833.       popup.addch(height - 4, width, curses.ACS_RTEE)
  834.       popup.hline(height - 4, 1, width - 1)
  835.       if isScrollbarVisible: popup.addch(height - 4, 2, curses.ACS_BTEE)
  836.       
  837.       # renders the selection options
  838.       confirmationMsg = "Run tor with the above configuration?"
  839.       popup.addstr(height - 3, width - len(confirmationMsg) - 1, confirmationMsg, uiTools.getColor("green") | curses.A_BOLD)
  840.       
  841.       drawX = width - 1
  842.       for i in range(len(options) - 1, -1, -1):
  843.         optionLabel = " %s " % options[i]
  844.         drawX -= (len(optionLabel) + 4)
  845.         
  846.         selectionFormat = curses.A_STANDOUT if i == selection else curses.A_NORMAL
  847.         popup.addstr(height - 2, drawX, "[", uiTools.getColor("green"))
  848.         popup.addstr(height - 2, drawX + 1, optionLabel, uiTools.getColor("green") | selectionFormat | curses.A_BOLD)
  849.         popup.addstr(height - 2, drawX + len(optionLabel) + 1, "]", uiTools.getColor("green"))
  850.         
  851.         drawX -= 1 # space gap between the options
  852.       
  853.       popup.win.refresh()
  854.       key = cli.controller.getController().getScreen().getch()
  855.       
  856.       if key == curses.KEY_LEFT:
  857.         selection = (selection - 1) % len(options)
  858.       elif key == curses.KEY_RIGHT:
  859.         selection = (selection + 1) % len(options)
  860.       elif uiTools.isScrollKey(key):
  861.         scroll = uiTools.getScrollPosition(key, scroll, height - 5, len(torrcLines))
  862.       elif uiTools.isSelectionKey(key):
  863.         if selection == 0: return CANCEL
  864.         elif selection == 1: return BACK
  865.         else: return NEXT
  866.       elif key in (27, ord('q'), ord('Q')): return CANCEL
  867.   finally:
  868.     cli.popups.finalize()
  869.  
  870. def _splitStr(msg, width):
  871.   """
  872.   Splits a string into substrings of a given length.
  873.   
  874.   Arguments:
  875.     msg   - string to be broken up
  876.     width - max length of any returned substring
  877.   """
  878.   
  879.   results = []
  880.   while msg:
  881.     msgSegment, msg = uiTools.cropStr(msg, width, None, endType = None, getRemainder = True)
  882.     if not msgSegment: break # happens if the width is less than the first word
  883.     results.append(msgSegment.strip())
  884.   
  885.   return results
  886.  
  887. def _isEmailAddress(address):
  888.   """
  889.   True if the input is an email address, false otherwise.
  890.   """
  891.   
  892.   # just checks if there's an '@' and '.' in the input w/o whitespace
  893.   emailMatcher = re.compile("\S*@\S*\.\S*")
  894.   return emailMatcher.match(address)
  895.  
  896. def _obscureEmailAddress(address):
  897.   """
  898.   Makes some effort to obscure an email address while keeping it readable.
  899.   
  900.   Arguments:
  901.     address - actual email address
  902.   """
  903.   
  904.   address = _obscureChar(address, '@', (_randomCase("at"), ))
  905.   address = _obscureChar(address, '.', (_randomCase("dot"), ))
  906.   return address
  907.  
  908. def _randomCase(word):
  909.   """
  910.   Provides a word back with the case of its letters randomized.
  911.   
  912.   Arguments:
  913.     word - word for which to randomize the case
  914.   """
  915.   
  916.   result = []
  917.   for letter in word:
  918.     result.append(random.choice((letter.lower(), letter.upper())))
  919.   
  920.   return "".join(result)
  921.  
  922. def _obscureChar(inputText, target, options):
  923.   """
  924.   Obscures the given character from the input, replacing it with something
  925.   from a set of options and bracketing the selection.
  926.   
  927.   Arguments:
  928.     inputText - text to be obscured
  929.     target    - character to be replaced
  930.     options   - replacement options for the character
  931.   """
  932.   
  933.   leftSpace = random.randint(0, 3)
  934.   leftFill = random.choice((' ', '_', '-', '=', '<'))
  935.   
  936.   rightSpace = random.randint(0, 3)
  937.   rightFill = random.choice((' ', '_', '-', '=', '>'))
  938.   
  939.   bracketLeft, bracketRight = random.choice(BRACKETS)
  940.   optSelection = random.choice(options)
  941.   replacement = "".join((bracketLeft, leftFill * leftSpace, optSelection, rightFill * rightSpace, bracketRight))
  942.   
  943.   return inputText.replace(target, replacement)
  944.  
  945. def _toggleEnabledAction(toggleOptions, option, value, invert = False):
  946.   """
  947.   Enables or disables custom exit policy options based on our selection.
  948.   
  949.   Arguments:
  950.     toggleOptions - configuration options to be toggled to match our our
  951.                     selection (ie, true -> enabled, false -> disabled)
  952.     options       - our config option
  953.     value         - the value we're being set to
  954.     invert        - inverts selection if true
  955.   """
  956.   
  957.   if invert: value = not value
  958.   
  959.   for opt in toggleOptions:
  960.     opt.setEnabled(value)
  961.  
  962. def _relayRateValidator(option, value):
  963.   if value.count(" ") != 1:
  964.     msg = "This should be a rate measurement (for instance, \"5 MB/s\")"
  965.     raise ValueError(msg)
  966.   
  967.   rate, units = value.split(" ", 1)
  968.   acceptedUnits = ("KB/s", "MB/s", "GB/s")
  969.   if not rate.isdigit():
  970.     raise ValueError("'%s' isn't an integer" % rate)
  971.   elif not units in acceptedUnits:
  972.     msg = "'%s' is an invalid rate, options include \"%s\"" % (units, "\", \"".join(acceptedUnits))
  973.     raise ValueError(msg)
  974.   elif (int(rate) < 20 and units == "KB/s") or int(rate) < 1:
  975.     raise ValueError("To be usable as a relay the rate must be at least 20 KB/s")
  976.  
  977. def _monthlyLimitValidator(option, value):
  978.   if value.count(" ") != 1:
  979.     msg = "This should be a traffic size (for instance, \"5 MB\")"
  980.     raise ValueError(msg)
  981.   
  982.   rate, units = value.split(" ", 1)
  983.   acceptedUnits = ("MB", "GB", "TB")
  984.   if not rate.isdigit():
  985.     raise ValueError("'%s' isn't an integer" % rate)
  986.   elif not units in acceptedUnits:
  987.     msg = "'%s' is an invalid unit, options include \"%s\"" % (units, "\", \"".join(acceptedUnits))
  988.     raise ValueError(msg)
  989.   elif (int(rate) < 50 and units == "MB") or int(rate) < 1:
  990.     raise ValueError("To be usable as a relay's monthly limit should be at least 50 MB")
  991.  
  992. def _bridgeDestinationValidator(option, value):
  993.   if value.count(":") != 1:
  994.     raise ValueError("Bridges are of the form '<ip address>:<port>'")
  995.   
  996.   ipAddr, port = value.split(":", 1)
  997.   if not connections.isValidIpAddress(ipAddr):
  998.     raise ValueError("'%s' is not a valid ip address" % ipAddr)
  999.   elif not port.isdigit() or int(port) < 0 or int(port) > 65535:
  1000.     raise ValueError("'%s' isn't a valid port number" % port)
  1001.  
  1002. def _circDurationValidator(option, value):
  1003.   if value.count(" ") != 1:
  1004.     msg = "This should be a time measurement (for instance, \"10 minutes\")"
  1005.     raise ValueError(msg)
  1006.   
  1007.   rate, units = value.split(" ", 1)
  1008.   acceptedUnits = ("minute", "minutes", "hour", "hours")
  1009.   if not rate.isdigit():
  1010.     raise ValueError("'%s' isn't an integer" % rate)
  1011.   elif not units in acceptedUnits:
  1012.     msg = "'%s' is an invalid rate, options include \"minutes\" or \"hours\""
  1013.     raise ValueError(msg)
  1014.   elif (int(rate) < 5 and units in ("minute", "minutes")) or int(rate) < 1:
  1015.     raise ValueError("This would cause high network load, don't set this to less than five minutes")
  1016.  
  1017.