commit aa3e3ee7a397edb18595d86dc2652d63053b3584
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon Sep 12 19:44:58 2011 -0700
Replacing displayed consensus policy with util
Using the ExitPolicy class' summary rather than the string from the consensus.
Unless I'm misunderstanding how the consensus policy is derived, these should
be equivilant. However, since this doesn't rely on the policy being in the
consensus this will be rendered the same for both …
[View More]new and old versions of tor.
---
src/cli/connections/connEntry.py | 20 ++++----------------
1 files changed, 4 insertions(+), 16 deletions(-)
diff --git a/src/cli/connections/connEntry.py b/src/cli/connections/connEntry.py
index 075dfb2..de3fb01 100644
--- a/src/cli/connections/connEntry.py
+++ b/src/cli/connections/connEntry.py
@@ -711,28 +711,16 @@ class ConnectionLine(entries.ConnectionPanelLine):
if len(nsLines) >= 2 and nsLines[1].startswith("s "):
flags = nsLines[1][2:]
- # The network status exit policy doesn't exist for older tor versions.
- # If unavailable we'll need the full exit policy which is on the
- # descriptor (if that's available).
+ exitPolicy = conn.getRelayExitPolicy(fingerprint)
- exitPolicy = "unknown"
- if len(nsLines) >= 4 and nsLines[3].startswith("p "):
- exitPolicy = nsLines[3][2:].replace(",", ", ")
- elif descEntry:
- # the descriptor has an individual line for each entry in the exit policy
- exitPolicyEntries = []
-
- for line in descEntry.split("\n"):
- if line.startswith("accept") or line.startswith("reject"):
- exitPolicyEntries.append(line.strip())
-
- exitPolicy = ", ".join(exitPolicyEntries)
+ if exitPolicy: policyLabel = exitPolicy.getSummary()
+ else: policyLabel = "unknown"
dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort
lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel)
lines[3] = "published: %s %s" % (pubDate, pubTime)
lines[4] = "flags: %s" % flags.replace(" ", ", ")
- lines[5] = "exit policy: %s" % exitPolicy
+ lines[5] = "exit policy: %s" % policyLabel
if descEntry:
torVersion, platform, contact = "", "", ""
[View Less]
commit 2828ee86dee7028bc2a1265a26204f54af45c181
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon Sep 12 10:07:52 2011 -0700
Util function for getting a relay's exit policy
Function that fetches a relay's exit policy, first using the descriptor and
falling back to its consensus entry. I'm a little concerned that obsolete
descriptor data can throw this off but not sure of a better method for doing
this...
---
src/util/torTools.py | 78 ++++++++++++++++…
[View More]++++++++++++++++++++++++++++++++--
1 files changed, 75 insertions(+), 3 deletions(-)
diff --git a/src/util/torTools.py b/src/util/torTools.py
index c81847c..7c14d18 100644
--- a/src/util/torTools.py
+++ b/src/util/torTools.py
@@ -14,7 +14,7 @@ import Queue
from TorCtl import TorCtl, TorUtil
-from util import enum, log, procTools, sysTools, uiTools
+from util import connections, enum, log, procTools, sysTools, uiTools
# enums for tor's controller state:
# INIT - attached to a new controller
@@ -1117,10 +1117,10 @@ class Controller(TorCtl.PostEventListener):
isPrivateRejected = self.getOption("ExitPolicyRejectPrivate", True)
if isPrivateRejected:
- result = ExitPolicy("reject private", result)
-
myAddress = self.getInfo("address")
if myAddress: result = ExitPolicy("reject %s" % myAddress, result)
+
+ result = ExitPolicy("reject private", result)
else:
# no ORPort is set so all relaying is disabled
result = ExitPolicy("reject *:*", None)
@@ -1250,6 +1250,78 @@ class Controller(TorCtl.PostEventListener):
return result
+ def getRelayExitPolicy(self, relayFingerprint, allowImprecision = True):
+ """
+ Provides the ExitPolicy instance associated with the given relay. The tor
+ consensus entries don't indicate if private addresses are rejected or
+ address-specific policies, so this is only used as a fallback if a recent
+ descriptor is unavailable. This returns None if unable to determine the
+ policy.
+
+ Arguments:
+ relayFingerprint - fingerprint of the relay
+ allowImprecision - make use of consensus policies as a fallback
+ """
+
+ self.connLock.acquire()
+
+ result = None
+ if self.isAlive():
+ # attempts to fetch the policy via the descriptor
+ descriptor = self.getDescriptorEntry(relayFingerprint)
+
+ if descriptor:
+ exitPolicyEntries = []
+ for line in descriptor.split("\n"):
+ if line.startswith("accept ") or line.startswith("reject "):
+ exitPolicyEntries.append(line)
+
+ # construct the policy chain
+ for entry in reversed(exitPolicyEntries):
+ result = ExitPolicy(entry, result)
+ elif allowImprecision:
+ # Falls back to using the consensus entry, which is both less precise
+ # and unavailable with older tor versions. This assumes that the relay
+ # has ExitPolicyRejectPrivate set and won't include address-specific
+ # policies.
+
+ consensusLine, relayAddress = None, None
+
+ nsEntry = self.getConsensusEntry(relayFingerprint)
+ if nsEntry:
+ for line in nsEntry.split("\n"):
+ if line.startswith("r "):
+ # fetch the relay's public address, which we'll need for the
+ # ExitPolicyRejectPrivate policy entry
+
+ lineComp = line.split(" ")
+ if len(lineComp) >= 7 and connections.isValidIpAddress(lineComp[6]):
+ relayAddress = lineComp[6]
+ elif line.startswith("p "):
+ consensusLine = line
+ break
+
+ if consensusLine:
+ acceptance, ports = consensusLine.split(" ")[1:]
+
+ # starts with a reject-all for whitelists and accept-all for blacklists
+ if acceptance == "accept":
+ result = ExitPolicy("reject *:*", None)
+ else:
+ result = ExitPolicy("accept *:*", None)
+
+ # adds each of the ports listed in the consensus
+ for port in reversed(ports.split(",")):
+ result = ExitPolicy("%s *:%s" % (acceptance, port), result)
+
+ # adds ExitPolicyRejectPrivate since this is the default
+ if relayAddress: result = ExitPolicy("reject %s" % relayAddress, result)
+ result = ExitPolicy("reject private", result)
+
+ self.connLock.release()
+
+ return result
+
def getRelayAddress(self, relayFingerprint, default = None):
"""
Provides the (IP Address, ORPort) tuple for a given relay. If the lookup
[View Less]
commit aaec744638d997447db3b4c50a3080d93835dd12
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sun Aug 28 17:41:17 2011 -0700
Tab completion for control prompt input
This provides autocompletion for the control prompt based on the capabilities
of the attached tor instance (fetching options from GETINFO info/names,
config/names, etc). This uses readline so it's working for the terminal prompt
but not the panel interpretor (which will need a validator …
[View More]implementation).
---
src/util/torInterpretor.py | 122 +++++++++++++++++++++++++++++++++++++++++--
1 files changed, 116 insertions(+), 6 deletions(-)
diff --git a/src/util/torInterpretor.py b/src/util/torInterpretor.py
index 866495d..fecc2ac 100644
--- a/src/util/torInterpretor.py
+++ b/src/util/torInterpretor.py
@@ -62,14 +62,115 @@ def format(msg, *attr):
if encodings:
return (CSI % ";".join(encodings)) + msg + RESET
- else:
- raise IOError("BLARG! %s" % str(attr))
- return msg
+ else: return msg
+
+class TorCommandOptions:
+ """
+ Command autocompleter, fetching the valid options from the attached Tor
+ instance.
+ """
+
+ def __init__(self):
+ self.commands = []
+ conn = torTools.getConn()
+
+ # adds all of the valid GETINFO options
+ infoOptions = conn.getInfo("info/names")
+ if infoOptions:
+ for line in infoOptions.split("\n"):
+ if " " in line:
+ # skipping non-existant options mentioned in:
+ # https://trac.torproject.org/projects/tor/ticket/3844
+
+ if line.startswith("config/*") or line.startswith("dir-usage"):
+ continue
+
+ # strips off the ending asterisk if it accepts a value
+ infoOpt = line.split(" ", 1)[0]
+
+ if infoOpt.endswith("*"):
+ infoOpt = infoOpt[:-1]
+
+ self.commands.append("GETINFO %s" % infoOpt)
+ else: self.commands.append("GETINFO ")
+
+ # adds all of the valid GETCONF / SETCONF / RESETCONF options
+ confOptions = conn.getInfo("config/names")
+ if confOptions:
+ # individual options are '<name> <type>' pairs
+ confEntries = [opt.split(" ", 1)[0] for opt in confOptions.split("\n")]
+ self.commands += ["GETCONF %s" % conf for conf in confEntries]
+ self.commands += ["SETCONF %s " % conf for conf in confEntries]
+ self.commands += ["RESETCONF %s" % conf for conf in confEntries]
+ else:
+ self.commands.append("GETCONF ")
+ self.commands.append("SETCONF ")
+ self.commands.append("RESETCONF ")
+
+ # adds all of the valid SETEVENT options
+ eventOptions = conn.getInfo("events/names")
+ if eventOptions:
+ self.commands += ["SETEVENT %s" % event for event in eventOptions.split(" ")]
+ else: self.commands.append("SETEVENT ")
+
+ # adds all of the valid USEFEATURE options
+ featureOptions = conn.getInfo("features/names")
+ if featureOptions:
+ self.commands += ["USEFEATURE %s" % feature for feature in featureOptions.split(" ")]
+ else: self.commands.append("USEFEATURE ")
+
+ # adds all of the valid SIGNAL options
+ # this can't yet be fetched dynamically, as per:
+ # https://trac.torproject.org/projects/tor/ticket/3842
+
+ signals = ("RELOAD", "SHUTDOWN", "DUMP", "DEBUG", "HALT", "HUP", "INT",
+ "USR1", "USR2", "TERM", "NEWNYM", "CLEARDNSCACHE")
+ self.commands += ["SIGNAL %s" % sig for sig in signals]
+
+ # shouldn't use AUTHENTICATE since we only provide the prompt with an
+ # authenticated controller connection
+ #self.commands.append("AUTHENTICATE")
+
+ # other options
+ self.commands.append("SAVECONF")
+ self.commands.append("MAPADDRESS ")
+ self.commands.append("EXTENDCIRCUIT ")
+ self.commands.append("SETCIRCUITPURPOSE ")
+ self.commands.append("SETROUTERPURPOSE ")
+ self.commands.append("ATTACHSTREAM ")
+ self.commands.append("+POSTDESCRIPTOR ") # TODO: needs to support multiline options for this (ugg)
+ self.commands.append("REDIRECTSTREAM ")
+ self.commands.append("CLOSESTREAM ")
+ self.commands.append("CLOSECIRCUIT ")
+ self.commands.append("RESOLVE ")
+ self.commands.append("PROTOCOLINFO ")
+ self.commands.append("+LOADCONF") # TODO: another multiline...
+ self.commands.append("TAKEOWNERSHIP")
+ self.commands.append("QUIT") # TODO: give a confirmation when the user does this?
+
+ def complete(self, text, state):
+ # provides case insensetive autocompletion options based on self.commands
+ for cmd in self.commands:
+ if cmd.lower().startswith(text.lower()):
+ if not state: return cmd
+ else: state -= 1
def prompt():
prompt = format(">>> ", Color.GREEN, Attr.BOLD)
input = ""
+ # sets up tab autocompetion
+ torCommands = TorCommandOptions()
+ readline.parse_and_bind("tab: complete")
+ readline.set_completer(torCommands.complete)
+
+ # Essentially disables autocompletion by word delimiters. This is because
+ # autocompletion options are full commands (ex. "GETINFO version") so we want
+ # "GETINFO" to match to all the options rather than be treated as a complete
+ # command by itself.
+
+ readline.set_completer_delims("\n")
+
formatMap = {} # mapping of Format to Color and Attr enums
formatMap[Formats.PROMPT] = (Attr.BOLD, Color.GREEN)
formatMap[Formats.INPUT] = (Color.CYAN, )
@@ -82,7 +183,13 @@ def prompt():
formatMap[Formats.ERROR] = (Attr.BOLD, Color.RED)
while input != "/quit":
- input = raw_input(prompt)
+ try:
+ input = raw_input(prompt)
+ except:
+ # moves cursor to the next line and terminates (most commonly
+ # KeyboardInterrupt and EOFErro)
+ print
+ break
_, outputEntry = handleQuery(input)
@@ -129,16 +236,19 @@ def handleQuery(input):
if " " in input: cmd, arg = input.split(" ", 1)
else: cmd, arg = input, ""
+ # makes commands uppercase to match the spec
+ cmd = cmd.upper()
+
inputEntry.append((cmd + " ", Formats.INPUT_CMD))
if arg: inputEntry.append((arg, Formats.INPUT_ARG))
- if cmd.upper() == "GETINFO":
+ if cmd == "GETINFO":
try:
response = conn.getInfo(arg, suppressExc = False)
outputEntry.append((response, Formats.OUTPUT))
except Exception, exc:
outputEntry.append((str(exc), Formats.ERROR))
- elif cmd.upper() == "SETCONF":
+ elif cmd == "SETCONF":
if "=" in arg:
param, value = arg.split("=", 1)
[View Less]
commit 7b58a20d69150d3628b758ddef7c014b740cb5de
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sun Aug 28 18:27:16 2011 -0700
Basic tab completion for interpretor panel
This performs tab completion in the interpretor panel when there's a single
match. This does not, however, provide suggestions or common prefix completion
yet. The suggestions in particular are gonna be a pita...
---
src/cli/interpretorPanel.py | 3 +++
src/util/textInput.py | 34 …
[View More]++++++++++++++++++++++++++++++++++
src/util/torInterpretor.py | 16 +++++++++++++++-
3 files changed, 52 insertions(+), 1 deletions(-)
diff --git a/src/cli/interpretorPanel.py b/src/cli/interpretorPanel.py
index 162fd19..95866db 100644
--- a/src/cli/interpretorPanel.py
+++ b/src/cli/interpretorPanel.py
@@ -61,8 +61,11 @@ class InterpretorPanel(panel.Panel):
self.redraw(True)
# intercepts input so user can cycle through the history
+ torCommands = torInterpretor.TorCommandOptions()
+
validator = textInput.BasicValidator()
validator = textInput.HistoryValidator(self.previousCommands, validator)
+ validator = textInput.TabCompleter(torCommands.getMatches, validator)
xOffset = len(torInterpretor.PROMPT)
if len(self.contents) > self.maxY - 1:
diff --git a/src/util/textInput.py b/src/util/textInput.py
index c3c229f..31a5305 100644
--- a/src/util/textInput.py
+++ b/src/util/textInput.py
@@ -148,3 +148,37 @@ class HistoryValidator(TextInputValidator):
return PASS
+class TabCompleter(TextInputValidator):
+ """
+ Provides tab completion based on the current input, finishing if there's only
+ a single match. This expects a functor that accepts the current input and
+ provides matches.
+ """
+
+ def __init__(self, completer, nextValidator = None):
+ TextInputValidator.__init__(self, nextValidator)
+
+ # functor that accepts a string and gives a list of matches
+ self.completer = completer
+
+ def handleKey(self, key, textbox):
+ # Matches against the tab key. The ord('\t') is nine, though strangely none
+ # of the curses.KEY_*TAB constants match this...
+ if key == 9:
+ matches = self.completer(textbox.gather().strip())
+
+ if len(matches) == 1:
+ # only a single match, fill it in
+ newInput = matches[0]
+ y, _ = textbox.win.getyx()
+ _, maxX = textbox.win.getmaxyx()
+ textbox.win.clear()
+ textbox.win.addstr(y, 0, newInput[:maxX - 1])
+ textbox.win.move(y, min(len(newInput), maxX - 1))
+ elif len(matches) > 1:
+ pass # TODO: somehow display matches... this is not gonna be fun
+
+ return None
+
+ return PASS
+
diff --git a/src/util/torInterpretor.py b/src/util/torInterpretor.py
index fecc2ac..cffe200 100644
--- a/src/util/torInterpretor.py
+++ b/src/util/torInterpretor.py
@@ -148,8 +148,22 @@ class TorCommandOptions:
self.commands.append("TAKEOWNERSHIP")
self.commands.append("QUIT") # TODO: give a confirmation when the user does this?
+ def getMatches(self, text):
+ """
+ Provides all options that match the given input.
+
+ Arguments:
+ text - user input text to be matched against
+ """
+
+ return [cmd for cmd in self.commands if cmd.lower().startswith(text.lower())]
+
def complete(self, text, state):
- # provides case insensetive autocompletion options based on self.commands
+ """
+ Provides case insensetive autocompletion options, acting as a functor for
+ the readlines set_completer function.
+ """
+
for cmd in self.commands:
if cmd.lower().startswith(text.lower()):
if not state: return cmd
[View Less]
commit d6dca592931b529435d769fbf993659523cb08d6
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sun Aug 28 18:38:17 2011 -0700
Common prefix tab completion for interpretor panel
Implementing common prefix autocompletion for the interpretor panel. Only thing
left is displaying the matches...
---
src/util/textInput.py | 17 ++++++++++++++---
1 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/src/util/textInput.py b/src/util/textInput.py
index …
[View More]31a5305..c0f01ce 100644
--- a/src/util/textInput.py
+++ b/src/util/textInput.py
@@ -4,6 +4,7 @@ These can be chained together with the first matching validator taking
precidence.
"""
+import os
import curses
PASS = -1
@@ -165,18 +166,28 @@ class TabCompleter(TextInputValidator):
# Matches against the tab key. The ord('\t') is nine, though strangely none
# of the curses.KEY_*TAB constants match this...
if key == 9:
- matches = self.completer(textbox.gather().strip())
+ currentContents = textbox.gather().strip()
+ matches = self.completer(currentContents)
+ newInput = None
if len(matches) == 1:
# only a single match, fill it in
newInput = matches[0]
+ elif len(matches) > 1:
+ # looks for a common prefix we can complete
+ commonPrefix = os.path.commonprefix(matches) # weird that this comes from path...
+
+ if commonPrefix != currentContents:
+ newInput = commonPrefix
+
+ # TODO: somehow display matches... this is not gonna be fun
+
+ if newInput:
y, _ = textbox.win.getyx()
_, maxX = textbox.win.getmaxyx()
textbox.win.clear()
textbox.win.addstr(y, 0, newInput[:maxX - 1])
textbox.win.move(y, min(len(newInput), maxX - 1))
- elif len(matches) > 1:
- pass # TODO: somehow display matches... this is not gonna be fun
return None
[View Less]
commit 42fee2eceb349dc04288b0b49a2a5dac17578878
Author: Damian Johnson <atagar(a)torproject.org>
Date: Tue Aug 30 19:09:55 2011 -0700
Interpretor function for writing backlog
Implementing interpretor '/write [path]' function. This simply dumps the
backlog to the given path. If no path is supplied then it uses the last
location written to.
---
src/util/torInterpretor.py | 33 +++++++++++++++++++++++++++++++--
1 files changed, 31 insertions(+), 2 deletions(-)
…
[View More]diff --git a/src/util/torInterpretor.py b/src/util/torInterpretor.py
index 266e9e4..0be511d 100644
--- a/src/util/torInterpretor.py
+++ b/src/util/torInterpretor.py
@@ -4,7 +4,7 @@ adds usability features like IRC style interpretor commands and, when ran
directly, history and tab completion.
"""
-import readline # simply importing this provides history to raw_input
+import readline
import version
@@ -193,6 +193,7 @@ class ControlInterpretor:
def __init__(self):
self.backlog = [] # prior requests the user has made
self.contents = [] # (msg, format list) tuples for what's been displayed
+ self.lastWritePath = "/tmp/torInterpretor_output"
def getBacklog(self):
"""
@@ -215,6 +216,24 @@ class ControlInterpretor:
return self.contents + [appendPrompt]
else: return self.contents
+ def writeContents(self, path):
+ """
+ Attempts to write the display contents to a given path, raising an IOError
+ if unsuccessful.
+
+ Arguments:
+ path - location to write the interpretor content to
+ """
+
+ outputLines = []
+
+ for line in self.contents:
+ outputLines.append("".join([msg for msg, _ in line]))
+
+ outputFile = open(path, "w")
+ outputFile.write("\n".join(outputLines))
+ outputFile.close()
+
def handleQuery(self, input):
"""
Processes the given input. Requests starting with a '/' are special
@@ -250,11 +269,21 @@ class ControlInterpretor:
if input == "/quit":
raise InterpretorClosed()
+ elif input.startswith("/write"):
+ if " " in input: writePath = input.split(" ", 1)[1]
+ else: writePath = self.lastWritePath
+
+ try:
+ self.writeContents(writePath)
+ outputEntry.append(("Interpretor backlog written to: %s" % writePath, OUTPUT_FORMAT))
+ except IOError, exc:
+ outputEntry.append(("Unable to write to '%s': %s" % (writePath, exc), ERROR_FORMAT))
+
+ self.lastWritePath = writePath
else:
outputEntry.append(("Not yet implemented...", ERROR_FORMAT)) # TODO: implement
# TODO: add /help option
- # TODO: add /write option
else:
# controller command
if " " in input: cmd, arg = input.split(" ", 1)
[View Less]