commit 8070fd004acde588f4d56d75bf6aa2c3f4d61b77 Author: Damian Johnson atagar@torproject.org Date: Wed Sep 21 09:33:28 2011 -0700
fix: using all controller authentication methods
This is a hacked up version of the fix for https://trac.torproject.org/projects/tor/ticket/3958
It's a bit of a Frankenstein and will be removed when the TorCtl fix is merged. --- src/starter.py | 9 ++- src/util/torTools.py | 181 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 4 deletions(-)
diff --git a/src/starter.py b/src/starter.py index dd5f5c9..4315f06 100644 --- a/src/starter.py +++ b/src/starter.py @@ -203,15 +203,16 @@ def _torCtlConnect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None, i
conn = None try: - conn, authType, authValue = TorCtl.TorCtl.preauth_connect(controlAddr, controlPort) + #conn, authType, authValue = TorCtl.TorCtl.preauth_connect(controlAddr, controlPort) + conn, authTypes, authValue = util.torTools.preauth_connect_alt(controlAddr, controlPort)
- if authType == TorCtl.TorCtl.AUTH_TYPE.PASSWORD: + if TorCtl.TorCtl.AUTH_TYPE.PASSWORD in authTypes: # password authentication, promting for the password if it wasn't provided if passphrase: authValue = passphrase else: try: authValue = getpass.getpass("Controller password: ") except KeyboardInterrupt: return None - elif authType == TorCtl.TorCtl.AUTH_TYPE.COOKIE and authValue[0] != "/": + elif TorCtl.TorCtl.AUTH_TYPE.COOKIE in authTypes and authValue[0] != "/": # Connecting to the control port will probably fail if it's using cookie # authentication and the cookie path is relative (unfortunately this is # the case for TBB). This is discussed in: @@ -226,7 +227,7 @@ def _torCtlConnect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None, i except IOError: pass
# appends the path prefix if it's set - if authType == TorCtl.TorCtl.AUTH_TYPE.COOKIE: + if TorCtl.TorCtl.AUTH_TYPE.COOKIE in authTypes: pathPrefix = util.torTools.getConn().getPathPrefix()
# The os.path.join function is kinda stupid. If given an absolute path diff --git a/src/util/torTools.py b/src/util/torTools.py index 8c7a1ed..e59b35e 100644 --- a/src/util/torTools.py +++ b/src/util/torTools.py @@ -359,6 +359,187 @@ def isTorRunning():
return False
+# ============================================================ +# TODO: Remove when TorCtl can handle multiple auth methods +# https://trac.torproject.org/projects/tor/ticket/3958 +# +# The following is a hacked up version of the fix in that ticket. +# ============================================================ + +class FixedConnection(TorCtl.Connection): + def __init__(self, sock): + TorCtl.Connection.__init__(self, sock) + self._authTypes = [] + + def get_auth_types(self): + """ + Provides the list of authentication types used for the control port. Each + are members of the AUTH_TYPE enumeration and return results will always + have at least one result. This raises an IOError if the query to + PROTOCOLINFO fails. + """ + + if not self._authTypes: + # check PROTOCOLINFO for authentication type + try: + authInfo = self.sendAndRecv("PROTOCOLINFO\r\n")[1][1] + except Exception, exc: + if exc.message: excMsg = ": %s" % exc + else: excMsg = "" + raise IOError("Unable to query PROTOCOLINFO for the authentication type%s" % excMsg) + + # parses the METHODS and COOKIEFILE entries for details we need to + # authenticate + + authTypes, cookiePath = [], None + + for entry in authInfo.split(): + if entry.startswith("METHODS="): + # Comma separated list of our authentication types. If we have + # multiple then any of them will work. + + methodsEntry = entry[8:] + + for authEntry in methodsEntry.split(","): + if authEntry == "NULL": + authTypes.append(TorCtl.AUTH_TYPE.NONE) + elif authEntry == "HASHEDPASSWORD": + authTypes.append(TorCtl.AUTH_TYPE.PASSWORD) + elif authEntry == "COOKIE": + authTypes.append(TorCtl.AUTH_TYPE.COOKIE) + else: + # not of a recognized authentication type (new addition to the + # control-spec?) + + raise IOError("Unrecognized authentication type: %s" % authEntry) + elif entry.startswith("COOKIEFILE="") and entry.endswith("""): + # Quoted path of the authentication cookie. This only exists if we're + # using cookie auth and, of course, doesn't account for chroot. + + cookiePath = entry[12:-1] + + # There should always be a AUTH METHODS entry. If we didn't then throw a + # wobbly. + + if not authTypes: + raise IOError("PROTOCOLINFO response didn't include any authentication methods") + + self._authType = authTypes[0] + self._authTypes = authTypes + self._cookiePath = cookiePath + + return list(self._authTypes) + + def authenticate(self, secret=""): + """ + Authenticates to the control port. If an issue arises this raises either of + the following: + - IOError for failures in reading an authentication cookie or querying + PROTOCOLINFO. + - TorCtl.ErrorReply for authentication failures or if the secret is + undefined when using password authentication + """ + + # fetches authentication type and cookie path if still unloaded + if not self._authTypes: self.get_auth_types() + + # validates input + if TorCtl.AUTH_TYPE.PASSWORD in self._authTypes and secret == "": + raise TorCtl.ErrorReply("Unable to authenticate: no passphrase provided") + + # tries each of our authentication methods, throwing the last exception if + # they all fail + + raisedExc = None + for authMethod in self._authTypes: + authCookie = None + try: + if authMethod == TorCtl.AUTH_TYPE.NONE: + self.authenticate_password("") + elif authMethod == TorCtl.AUTH_TYPE.PASSWORD: + self.authenticate_password(secret) + else: + authCookie = open(self._cookiePath, "r") + self.authenticate_cookie(authCookie) + authCookie.close() + + # Did the above raise an exception? No? Cool, we're done. + return + except TorCtl.ErrorReply, exc: + if authCookie: authCookie.close() + issue = str(exc) + + # simplifies message if the wrong credentials were provided (common + # mistake) + if issue.startswith("515 Authentication failed: "): + if issue[27:].startswith("Password did not match"): + issue = "password incorrect" + elif issue[27:] == "Wrong length on authentication cookie.": + issue = "cookie value incorrect" + + raisedExc = TorCtl.ErrorReply("Unable to authenticate: %s" % issue) + except IOError, exc: + if authCookie: authCookie.close() + issue = None + + # cleaner message for common errors + if str(exc).startswith("[Errno 13] Permission denied"): + issue = "permission denied" + elif str(exc).startswith("[Errno 2] No such file or directory"): + issue = "file doesn't exist" + + # if problem's recognized give concise message, otherwise print exception + # string + if issue: raisedExc = IOError("Failed to read authentication cookie (%s): %s" % (issue, self._cookiePath)) + else: raisedExc = IOError("Failed to read authentication cookie: %s" % exc) + + # if we got to this point then we failed to authenticate and should have a + # raisedExc + + if raisedExc: raise raisedExc + +def preauth_connect_alt(controlAddr="127.0.0.1", controlPort=9051, + ConnClass=FixedConnection): + """ + Provides an uninitiated torctl connection components for the control port, + returning a tuple of the form... + (torctl connection, authTypes, authValue) + + The authValue corresponds to the cookie path if using an authentication + cookie, otherwise this is the empty string. This raises an IOError in case + of failure. + + Arguments: + controlAddr - ip address belonging to the controller + controlPort - port belonging to the controller + ConnClass - connection type to instantiate + """ + + conn = None + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((controlAddr, controlPort)) + conn = ConnClass(s) + authTypes, authValue = conn.get_auth_types(), "" + + if TorCtl.AUTH_TYPE.COOKIE in authTypes: + authValue = conn.get_auth_cookie_path() + + return (conn, authTypes, authValue) + except socket.error, exc: + if conn: conn.close() + + if "Connection refused" in exc.args: + # most common case - tor control port isn't available + raise IOError("Connection refused. Is the ControlPort enabled?") + + raise IOError("Failed to establish socket: %s" % exc) + except Exception, exc: + if conn: conn.close() + raise IOError(exc) + +# ============================================================ + def getConn(): """ Singleton constructor for a Controller. Be aware that this starts as being