tor-commits
Threads by month
- ----- 2025 -----
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
May 2012
- 20 participants
- 844 discussions
commit 35307a623fd3de146ef11e85ae564b3508f6b87b
Author: Translation commit bot <translation(a)torproject.org>
Date: Tue May 29 10:45:09 2012 +0000
Update translations for tsum
---
ru/short-user-manual_ru_noimg.xhtml | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
diff --git a/ru/short-user-manual_ru_noimg.xhtml b/ru/short-user-manual_ru_noimg.xhtml
index 5cba14a..9a8817f 100644
--- a/ru/short-user-manual_ru_noimg.xhtml
+++ b/ru/short-user-manual_ru_noimg.xhtml
@@ -13,7 +13,7 @@
<p>Всё, что вы посылаете в Интернет через Tor, становится, во-первых анонимным (скрывается источник), а во-вторых, остаётся зашифрованным на всём пути между Вашим компьютером и последним ретранслятором. Но после того, как данные покидают последний ретранслятор и отправляются к интернет-адресу своего назначения - они идут уже не в зашифрованном, а обычном, открытом виде.</p>
<p>Если вы передаёте особо важные данные - например, вводите логин и пароль для входа на веб-сайт - убедитесь, что работает протокол HTTPS (т.е. в адресной строке написано, например, <strong>https</strong>://torproject.org/, а не <strong>http</strong>://torproject.org/).</p>
<h2 id="how-to-download-tor">Как загрузить Tor</h2>
- <p>The bundle we recommend to most users is the <a href="https://www.torproject.org/projects/torbrowser.html">Tor Browser Bundle</a>. This bundle contains a browser preconfigured to safely browse the Internet through Tor, and requires no installation. You download the bundle, extract the archive, and start Tor.</p>
+ <p>Пакет программ, который мы обычно рекомендуем - это <a href="https://www.torproject.org/projects/torbrowser.html">Tor Browser Bundle</a>. Этот пакет не требует установки и содержит браузер, заранее настроенный таким образом, чтобы безопасно выходить в интернет через Tor. Вы просто загружаете пакет, распаковываете архив и запускаете Tor.</p>
<p>There are two different ways to get hold of the Tor software. You can either browse to the <a href="https://www.torproject.org/">Tor Project website</a> and download it there, or you can use GetTor, the email autoresponder.</p>
<h3 id="how-to-get-tor-via-email">How to get Tor via email</h3>
<p>To receive the English Tor Browser Bundle for Windows, send an email to gettor(a)torproject.org with <strong>windows</strong> in the body of the message. You can leave the subject blank.</p>
1
0
commit 72aed261dceb560d05563fab1af4e6f549e2bdfd
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 15:23:52 2012 -0700
Single retry for socket connection
I just had integ tests fail due to an interrupt while trying to connect to the
tor control socket. This is the first time that I've seen it, so this isn't
much of a concern, but connecting to a socket is idempotent so we can do with
retrying it once if we fail.
---
stem/socket.py | 10 +++++++++-
1 files changed, 9 insertions(+), 1 deletions(-)
diff --git a/stem/socket.py b/stem/socket.py
index 6f0bfda..5abe902 100644
--- a/stem/socket.py
+++ b/stem/socket.py
@@ -206,7 +206,15 @@ class ControlSocket:
self._socket_file = self._socket.makefile()
self._is_alive = True
- self._connect()
+ # It's possable for this to have a transient failure...
+ # SocketError: [Errno 4] Interrupted system call
+ #
+ # It's safe to retry, so give it another try if it fails.
+
+ try:
+ self._connect()
+ except SocketError:
+ self._connect() # single retry
def close(self):
"""
1
0

29 May '12
commit 152059f7d687c93326e039c954bbfa72a9356323
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 15:43:35 2012 -0700
Adding an is_ok() method to controller responses
Method added by Ravi during his safecookie work. Stealing it a little early
since I want it for refactoring that I'm doing.
---
stem/socket.py | 13 +++++++++++++
1 files changed, 13 insertions(+), 0 deletions(-)
diff --git a/stem/socket.py b/stem/socket.py
index 5abe902..a9e2068 100644
--- a/stem/socket.py
+++ b/stem/socket.py
@@ -408,6 +408,19 @@ class ControlMessage:
self._parsed_content = parsed_content
self._raw_content = raw_content
+ def is_ok(self):
+ """
+ Checks if all of our lines have a 250 response.
+
+ Returns:
+ True if all lines have a 250 response code, False otherwise
+ """
+
+ for code, _, _ in self._parsed_content:
+ if code != "250": return False
+
+ return True
+
def content(self):
"""
Provides the parsed message content. These are entries of the form...
1
0

29 May '12
commit 1d7ab654ec473a5e54ea3253f82abdcadc93158f
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 15:12:16 2012 -0700
Moving tor response classes into their own module
As we add more response classes it'll be messy to sprinkle them all about the
codebase. Making a single 'stem.response' module that'll contain them all.
These could probably do with some more love so I'll next see if I can make them
any tidier.
---
run_tests.py | 12 +-
stem/__init__.py | 2 +-
stem/connection.py | 164 +-----------------------------
stem/control.py | 67 +------------
stem/response/__init__.py | 46 +++++++++
stem/response/getinfo.py | 43 ++++++++
stem/response/protocolinfo.py | 131 ++++++++++++++++++++++++
test/integ/connection/__init__.py | 2 +-
test/integ/connection/protocolinfo.py | 138 -------------------------
test/integ/response/__init__.py | 6 +
test/integ/response/protocolinfo.py | 138 +++++++++++++++++++++++++
test/mocking.py | 8 +-
test/unit/connection/__init__.py | 2 +-
test/unit/connection/protocolinfo.py | 178 --------------------------------
test/unit/control/getinfo.py | 119 ----------------------
test/unit/response/__init__.py | 6 +
test/unit/response/getinfo.py | 120 ++++++++++++++++++++++
test/unit/response/protocolinfo.py | 180 +++++++++++++++++++++++++++++++++
18 files changed, 689 insertions(+), 673 deletions(-)
diff --git a/run_tests.py b/run_tests.py
index 13fb027..dea86c3 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -16,13 +16,13 @@ import test.output
import test.runner
import test.check_whitespace
import test.unit.connection.authentication
-import test.unit.connection.protocolinfo
-import test.unit.control.getinfo
import test.unit.socket.control_line
import test.unit.socket.control_message
import test.unit.descriptor.reader
import test.unit.descriptor.server_descriptor
import test.unit.descriptor.extrainfo_descriptor
+import test.unit.response.getinfo
+import test.unit.response.protocolinfo
import test.unit.util.conf
import test.unit.util.connection
import test.unit.util.enum
@@ -31,7 +31,6 @@ import test.unit.util.tor_tools
import test.unit.version
import test.integ.connection.authentication
import test.integ.connection.connect
-import test.integ.connection.protocolinfo
import test.integ.control.base_controller
import test.integ.control.controller
import test.integ.socket.control_message
@@ -39,6 +38,7 @@ import test.integ.socket.control_socket
import test.integ.descriptor.reader
import test.integ.descriptor.server_descriptor
import test.integ.descriptor.extrainfo_descriptor
+import test.integ.response.protocolinfo
import test.integ.util.conf
import test.integ.util.system
import test.integ.process
@@ -100,11 +100,11 @@ UNIT_TESTS = (
test.unit.descriptor.server_descriptor.TestServerDescriptor,
test.unit.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor,
test.unit.version.TestVersion,
+ test.unit.response.getinfo.TestGetInfoResponse,
+ test.unit.response.protocolinfo.TestProtocolInfoResponse,
test.unit.socket.control_message.TestControlMessage,
test.unit.socket.control_line.TestControlLine,
test.unit.connection.authentication.TestAuthenticate,
- test.unit.connection.protocolinfo.TestProtocolInfoResponse,
- test.unit.control.getinfo.TestGetInfoResponse,
)
INTEG_TESTS = (
@@ -114,10 +114,10 @@ INTEG_TESTS = (
test.integ.descriptor.server_descriptor.TestServerDescriptor,
test.integ.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor,
test.integ.version.TestVersion,
+ test.integ.response.protocolinfo.TestProtocolInfo,
test.integ.process.TestProcess,
test.integ.socket.control_socket.TestControlSocket,
test.integ.socket.control_message.TestControlMessage,
- test.integ.connection.protocolinfo.TestProtocolInfo,
test.integ.connection.authentication.TestAuthenticate,
test.integ.connection.connect.TestConnect,
test.integ.control.base_controller.TestBaseController,
diff --git a/stem/__init__.py b/stem/__init__.py
index ceb137f..30e4f81 100644
--- a/stem/__init__.py
+++ b/stem/__init__.py
@@ -2,5 +2,5 @@
Library for working with the tor process.
"""
-__all__ = ["descriptor", "util", "connection", "control", "process", "socket", "version"]
+__all__ = ["descriptor", "response", "util", "connection", "control", "process", "socket", "version"]
diff --git a/stem/connection.py b/stem/connection.py
index 983eb84..34c9c68 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -48,14 +48,6 @@ authenticate_password - Authenticates to a socket supporting password auth.
authenticate_cookie - Authenticates to a socket supporting cookie auth.
get_protocolinfo - Issues a PROTOCOLINFO query.
-ProtocolInfoResponse - Reply from a PROTOCOLINFO query.
- |- Attributes:
- | |- protocol_version
- | |- tor_version
- | |- auth_methods
- | |- unknown_auth_methods
- | +- cookie_path
- +- convert - parses a ControlMessage, turning it into a ProtocolInfoResponse
AuthenticationFailure - Base exception raised for authentication failures.
|- UnrecognizedAuthMethods - Authentication methods are unsupported.
@@ -84,6 +76,7 @@ import os
import getpass
import binascii
+import stem.response
import stem.socket
import stem.version
import stem.util.enum
@@ -295,7 +288,7 @@ def authenticate(controller, password = None, chroot_path = None, protocolinfo_r
password (str) - passphrase to present to the socket if it uses password
authentication (skips password auth if None)
chroot_path (str) - path prefix if in a chroot environment
- protocolinfo_response (stem.connection.ProtocolInfoResponse) -
+ protocolinfo_response (stem.response.protocolinfo.ProtocolInfoResponse) -
tor protocolinfo response, this is retrieved on our own if None
Raises:
@@ -640,7 +633,7 @@ def get_protocolinfo(controller):
tor controller connection
Returns:
- stem.connection.ProtocolInfoResponse provided by tor
+ stem.response.protocolinfo.ProtocolInfoResponse provided by tor
Raises:
stem.socket.ProtocolError if the PROTOCOLINFO response is malformed
@@ -664,7 +657,7 @@ def get_protocolinfo(controller):
except stem.socket.SocketClosed, exc:
raise stem.socket.SocketError(exc)
- ProtocolInfoResponse.convert(protocolinfo_response)
+ stem.response.convert("PROTOCOLINFO", protocolinfo_response)
# attempt to expand relative cookie paths via the control port or socket file
@@ -723,152 +716,3 @@ def _expand_cookie_path(protocolinfo_response, pid_resolver, pid_resolution_arg)
protocolinfo_response.cookie_path = cookie_path
-class ProtocolInfoResponse(stem.socket.ControlMessage):
- """
- Version one PROTOCOLINFO query response.
-
- According to the control spec the cookie_file is an absolute path. However,
- this often is not the case (especially for the Tor Browser Bundle)...
- https://trac.torproject.org/projects/tor/ticket/1101
-
- If the path is relative then we'll make an attempt (which may not work) to
- correct this.
-
- The protocol_version is the only mandatory data for a valid PROTOCOLINFO
- response, so all other values are None if undefined or empty if a collection.
-
- Attributes:
- protocol_version (int) - protocol version of the response
- tor_version (stem.version.Version) - version of the tor process
- auth_methods (tuple) - AuthMethod types that tor will accept
- unknown_auth_methods (tuple) - strings of unrecognized auth methods
- cookie_path (str) - path of tor's authentication cookie
- """
-
- def convert(control_message):
- """
- Parses a ControlMessage, performing an in-place conversion of it into a
- ProtocolInfoResponse.
-
- Arguments:
- control_message (stem.socket.ControlMessage) -
- message to be parsed as a PROTOCOLINFO reply
-
- Raises:
- stem.socket.ProtocolError the message isn't a proper PROTOCOLINFO response
- TypeError if argument isn't a ControlMessage
- """
-
- if isinstance(control_message, stem.socket.ControlMessage):
- control_message.__class__ = ProtocolInfoResponse
- control_message._parse_message()
- return control_message
- else:
- raise TypeError("Only able to convert stem.socket.ControlMessage instances")
-
- convert = staticmethod(convert)
-
- def _parse_message(self):
- # Example:
- # 250-PROTOCOLINFO 1
- # 250-AUTH METHODS=COOKIE COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
- # 250-VERSION Tor="0.2.1.30"
- # 250 OK
-
- self.protocol_version = None
- self.tor_version = None
- self.cookie_path = None
-
- auth_methods, unknown_auth_methods = [], []
-
- # sanity check that we're a PROTOCOLINFO response
- if not list(self)[0].startswith("PROTOCOLINFO"):
- msg = "Message is not a PROTOCOLINFO response (%s)" % self
- raise stem.socket.ProtocolError(msg)
-
- for line in self:
- if line == "OK": break
- elif line.is_empty(): continue # blank line
-
- line_type = line.pop()
-
- if line_type == "PROTOCOLINFO":
- # Line format:
- # FirstLine = "PROTOCOLINFO" SP PIVERSION CRLF
- # PIVERSION = 1*DIGIT
-
- if line.is_empty():
- msg = "PROTOCOLINFO response's initial line is missing the protocol version: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- piversion = line.pop()
-
- if not piversion.isdigit():
- msg = "PROTOCOLINFO response version is non-numeric: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- self.protocol_version = int(piversion)
-
- # The piversion really should be "1" but, according to the spec, tor
- # does not necessarily need to provide the PROTOCOLINFO version that we
- # requested. Log if it's something we aren't expecting but still make
- # an effort to parse like a v1 response.
-
- if self.protocol_version != 1:
- log.info("We made a PROTOCOLINFO version 1 query but got a version %i response instead. We'll still try to use it, but this may cause problems." % self.protocol_version)
- elif line_type == "AUTH":
- # Line format:
- # AuthLine = "250-AUTH" SP "METHODS=" AuthMethod *("," AuthMethod)
- # *(SP "COOKIEFILE=" AuthCookieFile) CRLF
- # AuthMethod = "NULL" / "HASHEDPASSWORD" / "COOKIE"
- # AuthCookieFile = QuotedString
-
- # parse AuthMethod mapping
- if not line.is_next_mapping("METHODS"):
- msg = "PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- for method in line.pop_mapping()[1].split(","):
- if method == "NULL":
- auth_methods.append(AuthMethod.NONE)
- elif method == "HASHEDPASSWORD":
- auth_methods.append(AuthMethod.PASSWORD)
- elif method == "COOKIE":
- auth_methods.append(AuthMethod.COOKIE)
- else:
- unknown_auth_methods.append(method)
- message_id = "stem.connection.unknown_auth_%s" % method
- log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %s" % method)
-
- # our auth_methods should have a single AuthMethod.UNKNOWN entry if
- # any unknown authentication methods exist
- if not AuthMethod.UNKNOWN in auth_methods:
- auth_methods.append(AuthMethod.UNKNOWN)
-
- # parse optional COOKIEFILE mapping (quoted and can have escapes)
- if line.is_next_mapping("COOKIEFILE", True, True):
- self.cookie_path = line.pop_mapping(True, True)[1]
-
- # attempt to expand relative cookie paths
- _expand_cookie_path(self, stem.util.system.get_pid_by_name, "tor")
- elif line_type == "VERSION":
- # Line format:
- # VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF
- # TorVersion = QuotedString
-
- if not line.is_next_mapping("Tor", True):
- msg = "PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- torversion = line.pop_mapping(True)[1]
-
- try:
- self.tor_version = stem.version.Version(torversion)
- except ValueError, exc:
- raise stem.socket.ProtocolError(exc)
- else:
- log.debug("unrecognized PROTOCOLINFO line type '%s', ignoring entry: %s" % (line_type, line))
-
- self.auth_methods = tuple(auth_methods)
- self.unknown_auth_methods = tuple(unknown_auth_methods)
-
diff --git a/stem/control.py b/stem/control.py
index ccb57f4..b188ea4 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -26,6 +26,7 @@ import time
import Queue
import threading
+import stem.response
import stem.socket
import stem.util.log as log
@@ -475,7 +476,7 @@ class Controller(BaseController):
if response.content()[0][0] != "250":
raise stem.socket.ControllerError(str(response))
- GetInfoResponse.convert(response)
+ stem.response.convert("GETINFO", response)
# error if we got back different parameters than we requested
requested_params = set(param)
@@ -495,67 +496,3 @@ class Controller(BaseController):
if default == UNDEFINED: raise exc
else: return default
-class GetInfoResponse(stem.socket.ControlMessage):
- """
- Reply for a GETINFO query.
-
- Attributes:
- values (dict) - mapping between the queried options and their values
- """
-
- def convert(control_message):
- """
- Parses a ControlMessage, performing an in-place conversion of it into a
- GetInfoResponse.
-
- Arguments:
- control_message (stem.socket.ControlMessage) -
- message to be parsed as a GETINFO reply
-
- Raises:
- stem.socket.ProtocolError the message isn't a proper GETINFO response
- TypeError if argument isn't a ControlMessage
- """
-
- if isinstance(control_message, stem.socket.ControlMessage):
- control_message.__class__ = GetInfoResponse
- control_message._parse_message()
- return control_message
- else:
- raise TypeError("Only able to convert stem.socket.ControlMessage instances")
-
- convert = staticmethod(convert)
-
- def _parse_message(self):
- # Example:
- # 250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
- # 250+config-text=
- # ControlPort 9051
- # DataDirectory /home/atagar/.tor
- # ExitPolicy reject *:*
- # Log notice stdout
- # Nickname Unnamed
- # ORPort 9050
- # .
- # 250 OK
-
- self.values = {}
-
- for line in self:
- if line == "OK": break
- elif not "=" in line:
- raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line)
-
- key, value = line.split("=", 1)
-
- # if the value is a multiline value then it *must* be of the form
- # '<key>=\n<value>'
-
- if "\n" in value:
- if value.startswith("\n"):
- value = value[1:]
- else:
- raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline: %s" % line)
-
- self.values[key] = value
-
diff --git a/stem/response/__init__.py b/stem/response/__init__.py
new file mode 100644
index 0000000..92b9846
--- /dev/null
+++ b/stem/response/__init__.py
@@ -0,0 +1,46 @@
+"""
+Parses replies from the control socket.
+
+converts - translates a ControlMessage into a particular response subclass
+"""
+
+__all__ = ["getinfo", "protocolinfo"]
+
+import stem.socket
+
+def convert(response_type, message):
+ """
+ Converts a ControlMessage into a particular kind of tor response. This does
+ an in-place conversion of the message from being a ControlMessage to a
+ subclass for its response type. Recognized types include...
+
+ * GETINFO
+ * PROTOCOLINFO
+
+ If the response_type isn't recognized then this is leaves it alone.
+
+ Arguments:
+ response_type (str) - type of tor response to convert to
+ message (stem.socket.ControlMessage) - message to be converted
+
+ Raises:
+ stem.socket.ProtocolError the message isn't a proper response of that type
+ TypeError if argument isn't a ControlMessage or response_type isn't
+ supported
+ """
+
+ import stem.response.getinfo
+ import stem.response.protocolinfo
+
+ if not isinstance(message, stem.socket.ControlMessage):
+ raise TypeError("Only able to convert stem.socket.ControlMessage instances")
+
+ if response_type == "GETINFO":
+ response_class = stem.response.getinfo.GetInfoResponse
+ elif response_type == "PROTOCOLINFO":
+ response_class = stem.response.protocolinfo.ProtocolInfoResponse
+ else: raise TypeError("Unsupported response type: %s" % response_type)
+
+ message.__class__ = response_class
+ message._parse_message()
+
diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py
new file mode 100644
index 0000000..a13a18f
--- /dev/null
+++ b/stem/response/getinfo.py
@@ -0,0 +1,43 @@
+import stem.socket
+
+class GetInfoResponse(stem.socket.ControlMessage):
+ """
+ Reply for a GETINFO query.
+
+ Attributes:
+ values (dict) - mapping between the queried options and their values
+ """
+
+ def _parse_message(self):
+ # Example:
+ # 250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
+ # 250+config-text=
+ # ControlPort 9051
+ # DataDirectory /home/atagar/.tor
+ # ExitPolicy reject *:*
+ # Log notice stdout
+ # Nickname Unnamed
+ # ORPort 9050
+ # .
+ # 250 OK
+
+ self.values = {}
+
+ for line in self:
+ if line == "OK": break
+ elif not "=" in line:
+ raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line)
+
+ key, value = line.split("=", 1)
+
+ # if the value is a multiline value then it *must* be of the form
+ # '<key>=\n<value>'
+
+ if "\n" in value:
+ if value.startswith("\n"):
+ value = value[1:]
+ else:
+ raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline: %s" % line)
+
+ self.values[key] = value
+
diff --git a/stem/response/protocolinfo.py b/stem/response/protocolinfo.py
new file mode 100644
index 0000000..4547a1d
--- /dev/null
+++ b/stem/response/protocolinfo.py
@@ -0,0 +1,131 @@
+import stem.connection
+import stem.socket
+import stem.version
+import stem.util.log as log
+
+class ProtocolInfoResponse(stem.socket.ControlMessage):
+ """
+ Version one PROTOCOLINFO query response.
+
+ According to the control spec the cookie_file is an absolute path. However,
+ this often is not the case (especially for the Tor Browser Bundle)...
+ https://trac.torproject.org/projects/tor/ticket/1101
+
+ If the path is relative then we'll make an attempt (which may not work) to
+ correct this.
+
+ The protocol_version is the only mandatory data for a valid PROTOCOLINFO
+ response, so all other values are None if undefined or empty if a collection.
+
+ Attributes:
+ protocol_version (int) - protocol version of the response
+ tor_version (stem.version.Version) - version of the tor process
+ auth_methods (tuple) - AuthMethod types that tor will accept
+ unknown_auth_methods (tuple) - strings of unrecognized auth methods
+ cookie_path (str) - path of tor's authentication cookie
+ """
+
+ def _parse_message(self):
+ # Example:
+ # 250-PROTOCOLINFO 1
+ # 250-AUTH METHODS=COOKIE COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
+ # 250-VERSION Tor="0.2.1.30"
+ # 250 OK
+
+ self.protocol_version = None
+ self.tor_version = None
+ self.cookie_path = None
+
+ auth_methods, unknown_auth_methods = [], []
+
+ # sanity check that we're a PROTOCOLINFO response
+ if not list(self)[0].startswith("PROTOCOLINFO"):
+ msg = "Message is not a PROTOCOLINFO response (%s)" % self
+ raise stem.socket.ProtocolError(msg)
+
+ for line in self:
+ if line == "OK": break
+ elif line.is_empty(): continue # blank line
+
+ line_type = line.pop()
+
+ if line_type == "PROTOCOLINFO":
+ # Line format:
+ # FirstLine = "PROTOCOLINFO" SP PIVERSION CRLF
+ # PIVERSION = 1*DIGIT
+
+ if line.is_empty():
+ msg = "PROTOCOLINFO response's initial line is missing the protocol version: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ piversion = line.pop()
+
+ if not piversion.isdigit():
+ msg = "PROTOCOLINFO response version is non-numeric: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ self.protocol_version = int(piversion)
+
+ # The piversion really should be "1" but, according to the spec, tor
+ # does not necessarily need to provide the PROTOCOLINFO version that we
+ # requested. Log if it's something we aren't expecting but still make
+ # an effort to parse like a v1 response.
+
+ if self.protocol_version != 1:
+ log.info("We made a PROTOCOLINFO version 1 query but got a version %i response instead. We'll still try to use it, but this may cause problems." % self.protocol_version)
+ elif line_type == "AUTH":
+ # Line format:
+ # AuthLine = "250-AUTH" SP "METHODS=" AuthMethod *("," AuthMethod)
+ # *(SP "COOKIEFILE=" AuthCookieFile) CRLF
+ # AuthMethod = "NULL" / "HASHEDPASSWORD" / "COOKIE"
+ # AuthCookieFile = QuotedString
+
+ # parse AuthMethod mapping
+ if not line.is_next_mapping("METHODS"):
+ msg = "PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ for method in line.pop_mapping()[1].split(","):
+ if method == "NULL":
+ auth_methods.append(stem.connection.AuthMethod.NONE)
+ elif method == "HASHEDPASSWORD":
+ auth_methods.append(stem.connection.AuthMethod.PASSWORD)
+ elif method == "COOKIE":
+ auth_methods.append(stem.connection.AuthMethod.COOKIE)
+ else:
+ unknown_auth_methods.append(method)
+ message_id = "stem.connection.unknown_auth_%s" % method
+ log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %s" % method)
+
+ # our auth_methods should have a single AuthMethod.UNKNOWN entry if
+ # any unknown authentication methods exist
+ if not stem.connection.AuthMethod.UNKNOWN in auth_methods:
+ auth_methods.append(stem.connection.AuthMethod.UNKNOWN)
+
+ # parse optional COOKIEFILE mapping (quoted and can have escapes)
+ if line.is_next_mapping("COOKIEFILE", True, True):
+ self.cookie_path = line.pop_mapping(True, True)[1]
+
+ # attempt to expand relative cookie paths
+ stem.connection._expand_cookie_path(self, stem.util.system.get_pid_by_name, "tor")
+ elif line_type == "VERSION":
+ # Line format:
+ # VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF
+ # TorVersion = QuotedString
+
+ if not line.is_next_mapping("Tor", True):
+ msg = "PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line
+ raise stem.socket.ProtocolError(msg)
+
+ torversion = line.pop_mapping(True)[1]
+
+ try:
+ self.tor_version = stem.version.Version(torversion)
+ except ValueError, exc:
+ raise stem.socket.ProtocolError(exc)
+ else:
+ log.debug("unrecognized PROTOCOLINFO line type '%s', ignoring entry: %s" % (line_type, line))
+
+ self.auth_methods = tuple(auth_methods)
+ self.unknown_auth_methods = tuple(unknown_auth_methods)
+
diff --git a/test/integ/connection/__init__.py b/test/integ/connection/__init__.py
index 90471c0..eec60e6 100644
--- a/test/integ/connection/__init__.py
+++ b/test/integ/connection/__init__.py
@@ -2,5 +2,5 @@
Integration tests for stem.connection.
"""
-__all__ = ["authenticate", "connect", "protocolinfo"]
+__all__ = ["authenticate", "connect"]
diff --git a/test/integ/connection/protocolinfo.py b/test/integ/connection/protocolinfo.py
deleted file mode 100644
index 728c746..0000000
--- a/test/integ/connection/protocolinfo.py
+++ /dev/null
@@ -1,138 +0,0 @@
-"""
-Integration tests for the stem.connection.ProtocolInfoResponse class and
-related functions.
-"""
-
-import unittest
-
-import test.runner
-import stem.socket
-import stem.connection
-import stem.util.system
-import test.mocking as mocking
-from test.integ.util.system import filter_system_call
-
-class TestProtocolInfo(unittest.TestCase):
- def setUp(self):
- test.runner.require_control(self)
- mocking.mock(stem.util.proc.is_available, mocking.return_false())
- mocking.mock(stem.util.system.is_available, mocking.return_true())
-
- def tearDown(self):
- mocking.revert_mocking()
-
- def test_parsing(self):
- """
- Makes a PROTOCOLINFO query and processes the response for our control
- connection.
- """
-
- control_socket = test.runner.get_runner().get_tor_socket(False)
- control_socket.send("PROTOCOLINFO 1")
- protocolinfo_response = control_socket.recv()
- stem.connection.ProtocolInfoResponse.convert(protocolinfo_response)
- control_socket.close()
-
- # according to the control spec the following _could_ differ or be
- # undefined but if that actually happens then it's gonna make people sad
-
- self.assertEqual(1, protocolinfo_response.protocol_version)
- self.assertNotEqual(None, protocolinfo_response.tor_version)
- self.assertNotEqual(None, protocolinfo_response.auth_methods)
-
- self.assert_matches_test_config(protocolinfo_response)
-
- def test_get_protocolinfo_path_expansion(self):
- """
- If we're running with the 'RELATIVE' target then test_parsing() will
- exercise cookie path expansion when we're able to query the pid by our
- prcess name. This test selectively disables system.call() so we exercise
- the expansion via our control port or socket file.
-
- This test is largely redundant with test_parsing() if we aren't running
- with the 'RELATIVE' target.
- """
-
- if test.runner.Torrc.PORT in test.runner.get_runner().get_options():
- cwd_by_port_lookup_prefixes = (
- stem.util.system.GET_PID_BY_PORT_NETSTAT,
- stem.util.system.GET_PID_BY_PORT_SOCKSTAT % "",
- stem.util.system.GET_PID_BY_PORT_LSOF,
- stem.util.system.GET_CWD_PWDX % "",
- "lsof -a -p ")
-
- mocking.mock(stem.util.system.call, filter_system_call(cwd_by_port_lookup_prefixes))
- control_socket = stem.socket.ControlPort(control_port = test.runner.CONTROL_PORT)
- else:
- cwd_by_socket_lookup_prefixes = (
- stem.util.system.GET_PID_BY_FILE_LSOF % "",
- stem.util.system.GET_CWD_PWDX % "",
- "lsof -a -p ")
-
- mocking.mock(stem.util.system.call, filter_system_call(cwd_by_socket_lookup_prefixes))
- control_socket = stem.socket.ControlSocketFile(test.runner.CONTROL_SOCKET_PATH)
-
- protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
- self.assert_matches_test_config(protocolinfo_response)
-
- # we should have a usable socket at this point
- self.assertTrue(control_socket.is_alive())
- control_socket.close()
-
- def test_multiple_protocolinfo_calls(self):
- """
- Tests making repeated PROTOCOLINFO queries. This use case is interesting
- because tor will shut down the socket and stem should transparently
- re-establish it.
- """
-
- with test.runner.get_runner().get_tor_socket(False) as control_socket:
- for i in range(5):
- protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
- self.assert_matches_test_config(protocolinfo_response)
-
- def test_pre_disconnected_query(self):
- """
- Tests making a PROTOCOLINFO query when previous use of the socket had
- already disconnected it.
- """
-
- with test.runner.get_runner().get_tor_socket(False) as control_socket:
- # makes a couple protocolinfo queries outside of get_protocolinfo first
- control_socket.send("PROTOCOLINFO 1")
- control_socket.recv()
-
- control_socket.send("PROTOCOLINFO 1")
- control_socket.recv()
-
- protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
- self.assert_matches_test_config(protocolinfo_response)
-
- def assert_matches_test_config(self, protocolinfo_response):
- """
- Makes assertions that the protocolinfo response's attributes match those of
- the test configuration.
- """
-
- runner = test.runner.get_runner()
- tor_options = runner.get_options()
- auth_methods, auth_cookie_path = [], None
-
- if test.runner.Torrc.COOKIE in tor_options:
- auth_methods.append(stem.connection.AuthMethod.COOKIE)
- chroot_path = runner.get_chroot()
- auth_cookie_path = runner.get_auth_cookie_path()
-
- if chroot_path and auth_cookie_path.startswith(chroot_path):
- auth_cookie_path = auth_cookie_path[len(chroot_path):]
-
- if test.runner.Torrc.PASSWORD in tor_options:
- auth_methods.append(stem.connection.AuthMethod.PASSWORD)
-
- if not auth_methods:
- auth_methods.append(stem.connection.AuthMethod.NONE)
-
- self.assertEqual((), protocolinfo_response.unknown_auth_methods)
- self.assertEqual(tuple(auth_methods), protocolinfo_response.auth_methods)
- self.assertEqual(auth_cookie_path, protocolinfo_response.cookie_path)
-
diff --git a/test/integ/response/__init__.py b/test/integ/response/__init__.py
new file mode 100644
index 0000000..2e3a991
--- /dev/null
+++ b/test/integ/response/__init__.py
@@ -0,0 +1,6 @@
+"""
+Integration tests for stem.response.
+"""
+
+__all__ = ["protocolinfo"]
+
diff --git a/test/integ/response/protocolinfo.py b/test/integ/response/protocolinfo.py
new file mode 100644
index 0000000..f5eb518
--- /dev/null
+++ b/test/integ/response/protocolinfo.py
@@ -0,0 +1,138 @@
+"""
+Integration tests for the stem.response.protocolinfo.ProtocolInfoResponse class
+and related functions.
+"""
+
+import unittest
+
+import test.runner
+import stem.socket
+import stem.connection
+import stem.util.system
+import test.mocking as mocking
+from test.integ.util.system import filter_system_call
+
+class TestProtocolInfo(unittest.TestCase):
+ def setUp(self):
+ test.runner.require_control(self)
+ mocking.mock(stem.util.proc.is_available, mocking.return_false())
+ mocking.mock(stem.util.system.is_available, mocking.return_true())
+
+ def tearDown(self):
+ mocking.revert_mocking()
+
+ def test_parsing(self):
+ """
+ Makes a PROTOCOLINFO query and processes the response for our control
+ connection.
+ """
+
+ control_socket = test.runner.get_runner().get_tor_socket(False)
+ control_socket.send("PROTOCOLINFO 1")
+ protocolinfo_response = control_socket.recv()
+ stem.response.convert("PROTOCOLINFO", protocolinfo_response)
+ control_socket.close()
+
+ # according to the control spec the following _could_ differ or be
+ # undefined but if that actually happens then it's gonna make people sad
+
+ self.assertEqual(1, protocolinfo_response.protocol_version)
+ self.assertNotEqual(None, protocolinfo_response.tor_version)
+ self.assertNotEqual(None, protocolinfo_response.auth_methods)
+
+ self.assert_matches_test_config(protocolinfo_response)
+
+ def test_get_protocolinfo_path_expansion(self):
+ """
+ If we're running with the 'RELATIVE' target then test_parsing() will
+ exercise cookie path expansion when we're able to query the pid by our
+ prcess name. This test selectively disables system.call() so we exercise
+ the expansion via our control port or socket file.
+
+ This test is largely redundant with test_parsing() if we aren't running
+ with the 'RELATIVE' target.
+ """
+
+ if test.runner.Torrc.PORT in test.runner.get_runner().get_options():
+ cwd_by_port_lookup_prefixes = (
+ stem.util.system.GET_PID_BY_PORT_NETSTAT,
+ stem.util.system.GET_PID_BY_PORT_SOCKSTAT % "",
+ stem.util.system.GET_PID_BY_PORT_LSOF,
+ stem.util.system.GET_CWD_PWDX % "",
+ "lsof -a -p ")
+
+ mocking.mock(stem.util.system.call, filter_system_call(cwd_by_port_lookup_prefixes))
+ control_socket = stem.socket.ControlPort(control_port = test.runner.CONTROL_PORT)
+ else:
+ cwd_by_socket_lookup_prefixes = (
+ stem.util.system.GET_PID_BY_FILE_LSOF % "",
+ stem.util.system.GET_CWD_PWDX % "",
+ "lsof -a -p ")
+
+ mocking.mock(stem.util.system.call, filter_system_call(cwd_by_socket_lookup_prefixes))
+ control_socket = stem.socket.ControlSocketFile(test.runner.CONTROL_SOCKET_PATH)
+
+ protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
+ self.assert_matches_test_config(protocolinfo_response)
+
+ # we should have a usable socket at this point
+ self.assertTrue(control_socket.is_alive())
+ control_socket.close()
+
+ def test_multiple_protocolinfo_calls(self):
+ """
+ Tests making repeated PROTOCOLINFO queries. This use case is interesting
+ because tor will shut down the socket and stem should transparently
+ re-establish it.
+ """
+
+ with test.runner.get_runner().get_tor_socket(False) as control_socket:
+ for i in range(5):
+ protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
+ self.assert_matches_test_config(protocolinfo_response)
+
+ def test_pre_disconnected_query(self):
+ """
+ Tests making a PROTOCOLINFO query when previous use of the socket had
+ already disconnected it.
+ """
+
+ with test.runner.get_runner().get_tor_socket(False) as control_socket:
+ # makes a couple protocolinfo queries outside of get_protocolinfo first
+ control_socket.send("PROTOCOLINFO 1")
+ control_socket.recv()
+
+ control_socket.send("PROTOCOLINFO 1")
+ control_socket.recv()
+
+ protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
+ self.assert_matches_test_config(protocolinfo_response)
+
+ def assert_matches_test_config(self, protocolinfo_response):
+ """
+ Makes assertions that the protocolinfo response's attributes match those of
+ the test configuration.
+ """
+
+ runner = test.runner.get_runner()
+ tor_options = runner.get_options()
+ auth_methods, auth_cookie_path = [], None
+
+ if test.runner.Torrc.COOKIE in tor_options:
+ auth_methods.append(stem.connection.AuthMethod.COOKIE)
+ chroot_path = runner.get_chroot()
+ auth_cookie_path = runner.get_auth_cookie_path()
+
+ if chroot_path and auth_cookie_path.startswith(chroot_path):
+ auth_cookie_path = auth_cookie_path[len(chroot_path):]
+
+ if test.runner.Torrc.PASSWORD in tor_options:
+ auth_methods.append(stem.connection.AuthMethod.PASSWORD)
+
+ if not auth_methods:
+ auth_methods.append(stem.connection.AuthMethod.NONE)
+
+ self.assertEqual((), protocolinfo_response.unknown_auth_methods)
+ self.assertEqual(tuple(auth_methods), protocolinfo_response.auth_methods)
+ self.assertEqual(auth_cookie_path, protocolinfo_response.cookie_path)
+
diff --git a/test/mocking.py b/test/mocking.py
index aa61c76..f17dcc9 100644
--- a/test/mocking.py
+++ b/test/mocking.py
@@ -18,7 +18,7 @@ Mocking Functions
Instance Constructors
get_message - stem.socket.ControlMessage
- get_protocolinfo_response - stem.connection.ProtocolInfoResponse
+ get_protocolinfo_response - stem.response.protocolinfo.ProtocolInfoResponse
"""
import inspect
@@ -26,7 +26,7 @@ import itertools
import StringIO
import __builtin__
-import stem.connection
+import stem.response
import stem.socket
# Once we've mocked a function we can't rely on its __module__ or __name__
@@ -223,11 +223,11 @@ def get_protocolinfo_response(**attributes):
attributes (dict) - attributes to customize the response with
Returns:
- stem.connection.ProtocolInfoResponse instance
+ stem.response.protocolinfo.ProtocolInfoResponse instance
"""
protocolinfo_response = get_message("250-PROTOCOLINFO 1\n250 OK")
- stem.connection.ProtocolInfoResponse.convert(protocolinfo_response)
+ stem.response.convert("PROTOCOLINFO", protocolinfo_response)
for attr in attributes:
protocolinfo_response.__dict__[attr] = attributes[attr]
diff --git a/test/unit/connection/__init__.py b/test/unit/connection/__init__.py
index 4eae0fa..7073319 100644
--- a/test/unit/connection/__init__.py
+++ b/test/unit/connection/__init__.py
@@ -2,5 +2,5 @@
Unit tests for stem.connection.
"""
-__all__ = ["authentication", "protocolinfo"]
+__all__ = ["authentication"]
diff --git a/test/unit/connection/protocolinfo.py b/test/unit/connection/protocolinfo.py
deleted file mode 100644
index 795d780..0000000
--- a/test/unit/connection/protocolinfo.py
+++ /dev/null
@@ -1,178 +0,0 @@
-"""
-Unit tests for the stem.connection.ProtocolInfoResponse class.
-"""
-
-import unittest
-
-import stem.connection
-import stem.socket
-import stem.version
-import stem.util.proc
-import stem.util.system
-import test.mocking as mocking
-
-NO_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=NULL
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-PASSWORD_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=HASHEDPASSWORD
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-COOKIE_AUTH = r"""250-PROTOCOLINFO 1
-250-AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie"
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-MULTIPLE_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-UNKNOWN_AUTH = """250-PROTOCOLINFO 1
-250-AUTH METHODS=MAGIC,HASHEDPASSWORD,PIXIE_DUST
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-MINIMUM_RESPONSE = """250-PROTOCOLINFO 5
-250 OK"""
-
-RELATIVE_COOKIE_PATH = r"""250-PROTOCOLINFO 1
-250-AUTH METHODS=COOKIE COOKIEFILE="./tor-browser_en-US/Data/control_auth_cookie"
-250-VERSION Tor="0.2.1.30"
-250 OK"""
-
-class TestProtocolInfoResponse(unittest.TestCase):
- def test_convert(self):
- """
- Exercises functionality of the convert method both when it works and
- there's an error.
- """
-
- # working case
- control_message = mocking.get_message(NO_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
-
- # now this should be a ProtocolInfoResponse (ControlMessage subclass)
- self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
- self.assertTrue(isinstance(control_message, stem.connection.ProtocolInfoResponse))
-
- # exercise some of the ControlMessage functionality
- raw_content = (NO_AUTH + "\n").replace("\n", "\r\n")
- self.assertEquals(raw_content, control_message.raw_content())
- self.assertTrue(str(control_message).startswith("PROTOCOLINFO 1"))
-
- # attempt to convert the wrong type
- self.assertRaises(TypeError, stem.connection.ProtocolInfoResponse.convert, "hello world")
-
- # attempt to convert a different message type
- bw_event_control_message = mocking.get_message("650 BW 32326 2856")
- self.assertRaises(stem.socket.ProtocolError, stem.connection.ProtocolInfoResponse.convert, bw_event_control_message)
-
- def test_no_auth(self):
- """
- Checks a response when there's no authentication.
- """
-
- control_message = mocking.get_message(NO_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
-
- self.assertEquals(1, control_message.protocol_version)
- self.assertEquals(stem.version.Version("0.2.1.30"), control_message.tor_version)
- self.assertEquals((stem.connection.AuthMethod.NONE, ), control_message.auth_methods)
- self.assertEquals((), control_message.unknown_auth_methods)
- self.assertEquals(None, control_message.cookie_path)
-
- def test_password_auth(self):
- """
- Checks a response with password authentication.
- """
-
- control_message = mocking.get_message(PASSWORD_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.PASSWORD, ), control_message.auth_methods)
-
- def test_cookie_auth(self):
- """
- Checks a response with cookie authentication and a path including escape
- characters.
- """
-
- control_message = mocking.get_message(COOKIE_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.COOKIE, ), control_message.auth_methods)
- self.assertEquals("/tmp/my data\\\"dir//control_auth_cookie", control_message.cookie_path)
-
- def test_multiple_auth(self):
- """
- Checks a response with multiple authentication methods.
- """
-
- control_message = mocking.get_message(MULTIPLE_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.COOKIE, stem.connection.AuthMethod.PASSWORD), control_message.auth_methods)
- self.assertEquals("/home/atagar/.tor/control_auth_cookie", control_message.cookie_path)
-
- def test_unknown_auth(self):
- """
- Checks a response with an unrecognized authtentication method.
- """
-
- control_message = mocking.get_message(UNKNOWN_AUTH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals((stem.connection.AuthMethod.UNKNOWN, stem.connection.AuthMethod.PASSWORD), control_message.auth_methods)
- self.assertEquals(("MAGIC", "PIXIE_DUST"), control_message.unknown_auth_methods)
-
- def test_minimum_response(self):
- """
- Checks a PROTOCOLINFO response that only contains the minimum amount of
- information to be a valid response.
- """
-
- control_message = mocking.get_message(MINIMUM_RESPONSE)
- stem.connection.ProtocolInfoResponse.convert(control_message)
-
- self.assertEquals(5, control_message.protocol_version)
- self.assertEquals(None , control_message.tor_version)
- self.assertEquals((), control_message.auth_methods)
- self.assertEquals((), control_message.unknown_auth_methods)
- self.assertEquals(None, control_message.cookie_path)
-
- def test_relative_cookie(self):
- """
- Checks an authentication cookie with a relative path where expansion both
- succeeds and fails.
- """
-
- # we need to mock both pid and cwd lookups since the general cookie
- # expanion works by...
- # - resolving the pid of the "tor" process
- # - using that to get tor's cwd
-
- def call_mocking(command):
- if command == stem.util.system.GET_PID_BY_NAME_PGREP % "tor":
- return ["10"]
- elif command == stem.util.system.GET_CWD_PWDX % 10:
- return ["10: /tmp/foo"]
-
- mocking.mock(stem.util.proc.is_available, mocking.return_false())
- mocking.mock(stem.util.system.is_available, mocking.return_true())
- mocking.mock(stem.util.system.call, call_mocking)
-
- control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals("/tmp/foo/tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
-
- # exercise cookie expansion where both calls fail (should work, just
- # leaving the path unexpanded)
-
- mocking.mock(stem.util.system.call, mocking.return_none())
- control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
- stem.connection.ProtocolInfoResponse.convert(control_message)
- self.assertEquals("./tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
-
- # reset system call mocking
- mocking.revert_mocking()
-
diff --git a/test/unit/control/getinfo.py b/test/unit/control/getinfo.py
deleted file mode 100644
index 3fc9fd3..0000000
--- a/test/unit/control/getinfo.py
+++ /dev/null
@@ -1,119 +0,0 @@
-"""
-Unit tests for the stem.control.GetInfoResponse class.
-"""
-
-import unittest
-
-import stem.connection
-import test.mocking as mocking
-
-EMPTY_RESPONSE = "250 OK"
-
-SINGLE_RESPONSE = """\
-250-version=0.2.3.11-alpha-dev
-250 OK"""
-
-BATCH_RESPONSE = """\
-250-version=0.2.3.11-alpha-dev
-250-address=67.137.76.214
-250-fingerprint=5FDE0422045DF0E1879A3738D09099EB4A0C5BA0
-250 OK"""
-
-MULTILINE_RESPONSE = """\
-250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
-250+config-text=
-ControlPort 9051
-DataDirectory /home/atagar/.tor
-ExitPolicy reject *:*
-Log notice stdout
-Nickname Unnamed
-ORPort 9050
-.
-250 OK"""
-
-NON_KEY_VALUE_ENTRY = """\
-250-version=0.2.3.11-alpha-dev
-250-address 67.137.76.214
-250 OK"""
-
-MISSING_MULTILINE_NEWLINE = """\
-250+config-text=ControlPort 9051
-DataDirectory /home/atagar/.tor
-.
-250 OK"""
-
-class TestGetInfoResponse(unittest.TestCase):
- def test_empty_response(self):
- """
- Parses a GETINFO reply without options (just calling "GETINFO").
- """
-
- control_message = mocking.get_message(EMPTY_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
-
- # now this should be a GetInfoResponse (ControlMessage subclass)
- self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
- self.assertTrue(isinstance(control_message, stem.control.GetInfoResponse))
-
- self.assertEqual({}, control_message.values)
-
- def test_single_response(self):
- """
- Parses a GETINFO reply response for a single parameter.
- """
-
- control_message = mocking.get_message(SINGLE_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
- self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.values)
-
- def test_batch_response(self):
- """
- Parses a GETINFO reply for muiltiple parameters.
- """
-
- control_message = mocking.get_message(BATCH_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
-
- expected = {
- "version": "0.2.3.11-alpha-dev",
- "address": "67.137.76.214",
- "fingerprint": "5FDE0422045DF0E1879A3738D09099EB4A0C5BA0",
- }
-
- self.assertEqual(expected, control_message.values)
-
- def test_multiline_response(self):
- """
- Parses a GETINFO reply for multiple parameters including a multi-line
- value.
- """
-
- control_message = mocking.get_message(MULTILINE_RESPONSE)
- stem.control.GetInfoResponse.convert(control_message)
-
- expected = {
- "version": "0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)",
- "config-text": "\n".join(MULTILINE_RESPONSE.splitlines()[2:8]),
- }
-
- self.assertEqual(expected, control_message.values)
-
- def test_invalid_non_mapping_content(self):
- """
- Parses a malformed GETINFO reply containing a line that isn't a key=value
- entry.
- """
-
- control_message = mocking.get_message(NON_KEY_VALUE_ENTRY)
- self.assertRaises(stem.socket.ProtocolError, stem.control.GetInfoResponse.convert, control_message)
-
- def test_invalid_multiline_content(self):
- """
- Parses a malformed GETINFO reply with a multi-line entry missing a newline
- between its key and value. This is a proper controller message, but
- malformed according to the GETINFO's spec.
- """
-
- control_message = mocking.get_message(MISSING_MULTILINE_NEWLINE)
- self.assertRaises(stem.socket.ProtocolError, stem.control.GetInfoResponse.convert, control_message)
-
diff --git a/test/unit/response/__init__.py b/test/unit/response/__init__.py
new file mode 100644
index 0000000..530274c
--- /dev/null
+++ b/test/unit/response/__init__.py
@@ -0,0 +1,6 @@
+"""
+Unit tests for stem.response.
+"""
+
+__all__ = ["getinfo", "protocolinfo"]
+
diff --git a/test/unit/response/getinfo.py b/test/unit/response/getinfo.py
new file mode 100644
index 0000000..5f5862a
--- /dev/null
+++ b/test/unit/response/getinfo.py
@@ -0,0 +1,120 @@
+"""
+Unit tests for the stem.response.getinfo.GetInfoResponse class.
+"""
+
+import unittest
+
+import stem.response
+import stem.response.getinfo
+import test.mocking as mocking
+
+EMPTY_RESPONSE = "250 OK"
+
+SINGLE_RESPONSE = """\
+250-version=0.2.3.11-alpha-dev
+250 OK"""
+
+BATCH_RESPONSE = """\
+250-version=0.2.3.11-alpha-dev
+250-address=67.137.76.214
+250-fingerprint=5FDE0422045DF0E1879A3738D09099EB4A0C5BA0
+250 OK"""
+
+MULTILINE_RESPONSE = """\
+250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
+250+config-text=
+ControlPort 9051
+DataDirectory /home/atagar/.tor
+ExitPolicy reject *:*
+Log notice stdout
+Nickname Unnamed
+ORPort 9050
+.
+250 OK"""
+
+NON_KEY_VALUE_ENTRY = """\
+250-version=0.2.3.11-alpha-dev
+250-address 67.137.76.214
+250 OK"""
+
+MISSING_MULTILINE_NEWLINE = """\
+250+config-text=ControlPort 9051
+DataDirectory /home/atagar/.tor
+.
+250 OK"""
+
+class TestGetInfoResponse(unittest.TestCase):
+ def test_empty_response(self):
+ """
+ Parses a GETINFO reply without options (just calling "GETINFO").
+ """
+
+ control_message = mocking.get_message(EMPTY_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+
+ # now this should be a GetInfoResponse (ControlMessage subclass)
+ self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
+ self.assertTrue(isinstance(control_message, stem.response.getinfo.GetInfoResponse))
+
+ self.assertEqual({}, control_message.values)
+
+ def test_single_response(self):
+ """
+ Parses a GETINFO reply response for a single parameter.
+ """
+
+ control_message = mocking.get_message(SINGLE_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+ self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.values)
+
+ def test_batch_response(self):
+ """
+ Parses a GETINFO reply for muiltiple parameters.
+ """
+
+ control_message = mocking.get_message(BATCH_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+
+ expected = {
+ "version": "0.2.3.11-alpha-dev",
+ "address": "67.137.76.214",
+ "fingerprint": "5FDE0422045DF0E1879A3738D09099EB4A0C5BA0",
+ }
+
+ self.assertEqual(expected, control_message.values)
+
+ def test_multiline_response(self):
+ """
+ Parses a GETINFO reply for multiple parameters including a multi-line
+ value.
+ """
+
+ control_message = mocking.get_message(MULTILINE_RESPONSE)
+ stem.response.convert("GETINFO", control_message)
+
+ expected = {
+ "version": "0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)",
+ "config-text": "\n".join(MULTILINE_RESPONSE.splitlines()[2:8]),
+ }
+
+ self.assertEqual(expected, control_message.values)
+
+ def test_invalid_non_mapping_content(self):
+ """
+ Parses a malformed GETINFO reply containing a line that isn't a key=value
+ entry.
+ """
+
+ control_message = mocking.get_message(NON_KEY_VALUE_ENTRY)
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "GETINFO", control_message)
+
+ def test_invalid_multiline_content(self):
+ """
+ Parses a malformed GETINFO reply with a multi-line entry missing a newline
+ between its key and value. This is a proper controller message, but
+ malformed according to the GETINFO's spec.
+ """
+
+ control_message = mocking.get_message(MISSING_MULTILINE_NEWLINE)
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "GETINFO", control_message)
+
diff --git a/test/unit/response/protocolinfo.py b/test/unit/response/protocolinfo.py
new file mode 100644
index 0000000..a17c58c
--- /dev/null
+++ b/test/unit/response/protocolinfo.py
@@ -0,0 +1,180 @@
+"""
+Unit tests for the stem.response.protocolinfo.ProtocolInfoResponse class.
+"""
+
+import unittest
+
+from stem.connection import AuthMethod
+import stem.socket
+import stem.version
+import stem.util.proc
+import stem.util.system
+import stem.response
+import stem.response.protocolinfo
+import test.mocking as mocking
+
+NO_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=NULL
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+PASSWORD_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=HASHEDPASSWORD
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+COOKIE_AUTH = r"""250-PROTOCOLINFO 1
+250-AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie"
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+MULTIPLE_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+UNKNOWN_AUTH = """250-PROTOCOLINFO 1
+250-AUTH METHODS=MAGIC,HASHEDPASSWORD,PIXIE_DUST
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+MINIMUM_RESPONSE = """250-PROTOCOLINFO 5
+250 OK"""
+
+RELATIVE_COOKIE_PATH = r"""250-PROTOCOLINFO 1
+250-AUTH METHODS=COOKIE COOKIEFILE="./tor-browser_en-US/Data/control_auth_cookie"
+250-VERSION Tor="0.2.1.30"
+250 OK"""
+
+class TestProtocolInfoResponse(unittest.TestCase):
+ def test_convert(self):
+ """
+ Exercises functionality of the convert method both when it works and
+ there's an error.
+ """
+
+ # working case
+ control_message = mocking.get_message(NO_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+
+ # now this should be a ProtocolInfoResponse (ControlMessage subclass)
+ self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
+ self.assertTrue(isinstance(control_message, stem.response.protocolinfo.ProtocolInfoResponse))
+
+ # exercise some of the ControlMessage functionality
+ raw_content = (NO_AUTH + "\n").replace("\n", "\r\n")
+ self.assertEquals(raw_content, control_message.raw_content())
+ self.assertTrue(str(control_message).startswith("PROTOCOLINFO 1"))
+
+ # attempt to convert the wrong type
+ self.assertRaises(TypeError, stem.response.convert, "PROTOCOLINFO", "hello world")
+
+ # attempt to convert a different message type
+ bw_event_control_message = mocking.get_message("650 BW 32326 2856")
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "PROTOCOLINFO", bw_event_control_message)
+
+ def test_no_auth(self):
+ """
+ Checks a response when there's no authentication.
+ """
+
+ control_message = mocking.get_message(NO_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+
+ self.assertEquals(1, control_message.protocol_version)
+ self.assertEquals(stem.version.Version("0.2.1.30"), control_message.tor_version)
+ self.assertEquals((AuthMethod.NONE, ), control_message.auth_methods)
+ self.assertEquals((), control_message.unknown_auth_methods)
+ self.assertEquals(None, control_message.cookie_path)
+
+ def test_password_auth(self):
+ """
+ Checks a response with password authentication.
+ """
+
+ control_message = mocking.get_message(PASSWORD_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.PASSWORD, ), control_message.auth_methods)
+
+ def test_cookie_auth(self):
+ """
+ Checks a response with cookie authentication and a path including escape
+ characters.
+ """
+
+ control_message = mocking.get_message(COOKIE_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.COOKIE, ), control_message.auth_methods)
+ self.assertEquals("/tmp/my data\\\"dir//control_auth_cookie", control_message.cookie_path)
+
+ def test_multiple_auth(self):
+ """
+ Checks a response with multiple authentication methods.
+ """
+
+ control_message = mocking.get_message(MULTIPLE_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.COOKIE, AuthMethod.PASSWORD), control_message.auth_methods)
+ self.assertEquals("/home/atagar/.tor/control_auth_cookie", control_message.cookie_path)
+
+ def test_unknown_auth(self):
+ """
+ Checks a response with an unrecognized authtentication method.
+ """
+
+ control_message = mocking.get_message(UNKNOWN_AUTH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals((AuthMethod.UNKNOWN, AuthMethod.PASSWORD), control_message.auth_methods)
+ self.assertEquals(("MAGIC", "PIXIE_DUST"), control_message.unknown_auth_methods)
+
+ def test_minimum_response(self):
+ """
+ Checks a PROTOCOLINFO response that only contains the minimum amount of
+ information to be a valid response.
+ """
+
+ control_message = mocking.get_message(MINIMUM_RESPONSE)
+ stem.response.convert("PROTOCOLINFO", control_message)
+
+ self.assertEquals(5, control_message.protocol_version)
+ self.assertEquals(None , control_message.tor_version)
+ self.assertEquals((), control_message.auth_methods)
+ self.assertEquals((), control_message.unknown_auth_methods)
+ self.assertEquals(None, control_message.cookie_path)
+
+ def test_relative_cookie(self):
+ """
+ Checks an authentication cookie with a relative path where expansion both
+ succeeds and fails.
+ """
+
+ # we need to mock both pid and cwd lookups since the general cookie
+ # expanion works by...
+ # - resolving the pid of the "tor" process
+ # - using that to get tor's cwd
+
+ def call_mocking(command):
+ if command == stem.util.system.GET_PID_BY_NAME_PGREP % "tor":
+ return ["10"]
+ elif command == stem.util.system.GET_CWD_PWDX % 10:
+ return ["10: /tmp/foo"]
+
+ mocking.mock(stem.util.proc.is_available, mocking.return_false())
+ mocking.mock(stem.util.system.is_available, mocking.return_true())
+ mocking.mock(stem.util.system.call, call_mocking)
+
+ control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals("/tmp/foo/tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
+
+ # exercise cookie expansion where both calls fail (should work, just
+ # leaving the path unexpanded)
+
+ mocking.mock(stem.util.system.call, mocking.return_none())
+ control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
+ stem.response.convert("PROTOCOLINFO", control_message)
+ self.assertEquals("./tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
+
+ # reset system call mocking
+ mocking.revert_mocking()
+
1
0

29 May '12
commit 6015799baf9c0e6d3a618c948c32c27b2d7e1fc3
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 15:48:32 2012 -0700
Being more anal about 'OK' status checking
Refusing to parse messages that lack a 250 response on all lines, and checking
for a 'OK' status on the last line (and only the last line).
---
stem/response/getinfo.py | 10 +++++++---
stem/response/protocolinfo.py | 11 ++++++-----
2 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py
index a13a18f..7cde9f1 100644
--- a/stem/response/getinfo.py
+++ b/stem/response/getinfo.py
@@ -23,9 +23,13 @@ class GetInfoResponse(stem.socket.ControlMessage):
self.values = {}
- for line in self:
- if line == "OK": break
- elif not "=" in line:
+ lines = list(self)
+
+ if not self.is_ok() or not lines.pop() == "OK":
+ raise stem.socket.ProtocolError("GETINFO response didn't have an OK status:\n%s" % self)
+
+ for line in lines:
+ if not "=" in line:
raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line)
key, value = line.split("=", 1)
diff --git a/stem/response/protocolinfo.py b/stem/response/protocolinfo.py
index 4547a1d..97ecfb7 100644
--- a/stem/response/protocolinfo.py
+++ b/stem/response/protocolinfo.py
@@ -37,16 +37,17 @@ class ProtocolInfoResponse(stem.socket.ControlMessage):
self.cookie_path = None
auth_methods, unknown_auth_methods = [], []
+ lines = list(self)
+
+ if not self.is_ok() or not lines.pop() == "OK":
+ raise stem.socket.ProtocolError("GETINFO response didn't have an OK status:\n%s" % self)
# sanity check that we're a PROTOCOLINFO response
- if not list(self)[0].startswith("PROTOCOLINFO"):
+ if not lines[0].startswith("PROTOCOLINFO"):
msg = "Message is not a PROTOCOLINFO response (%s)" % self
raise stem.socket.ProtocolError(msg)
- for line in self:
- if line == "OK": break
- elif line.is_empty(): continue # blank line
-
+ for line in lines:
line_type = line.pop()
if line_type == "PROTOCOLINFO":
1
0

29 May '12
commit be06640765510b42912f074ebad6acb7cf4d0bac
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 15:52:18 2012 -0700
Renaming getinfo 'values' attribute to 'entries'
The name 'values' has bugged me for a while because it's a dictionary, so users
might end up typing things like "my_getinfo_response.values.values()". Renaming
it to something that's still generic but without these issues.
---
stem/control.py | 6 +++---
stem/response/getinfo.py | 10 +++++-----
test/unit/response/getinfo.py | 8 ++++----
3 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/stem/control.py b/stem/control.py
index b188ea4..1f3b767 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -480,7 +480,7 @@ class Controller(BaseController):
# error if we got back different parameters than we requested
requested_params = set(param)
- reply_params = set(response.values.keys())
+ reply_params = set(response.entries.keys())
if requested_params != reply_params:
requested_label = ", ".join(requested_params)
@@ -489,9 +489,9 @@ class Controller(BaseController):
raise stem.socket.ProtocolError("GETINFO reply doesn't match the parameters that we requested. Queried '%s' but got '%s'." % (requested_label, reply_label))
if is_multiple:
- return response.values
+ return response.entries
else:
- return response.values[param[0]]
+ return response.entries[param[0]]
except stem.socket.ControllerError, exc:
if default == UNDEFINED: raise exc
else: return default
diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py
index 7cde9f1..dabbe12 100644
--- a/stem/response/getinfo.py
+++ b/stem/response/getinfo.py
@@ -5,7 +5,7 @@ class GetInfoResponse(stem.socket.ControlMessage):
Reply for a GETINFO query.
Attributes:
- values (dict) - mapping between the queried options and their values
+ entries (dict) - mapping between the queried options and their values
"""
def _parse_message(self):
@@ -21,7 +21,7 @@ class GetInfoResponse(stem.socket.ControlMessage):
# .
# 250 OK
- self.values = {}
+ self.entries = {}
lines = list(self)
@@ -30,7 +30,7 @@ class GetInfoResponse(stem.socket.ControlMessage):
for line in lines:
if not "=" in line:
- raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line)
+ raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings:\n%s" % self)
key, value = line.split("=", 1)
@@ -41,7 +41,7 @@ class GetInfoResponse(stem.socket.ControlMessage):
if value.startswith("\n"):
value = value[1:]
else:
- raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline: %s" % line)
+ raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline:\n%s" % self)
- self.values[key] = value
+ self.entries[key] = value
diff --git a/test/unit/response/getinfo.py b/test/unit/response/getinfo.py
index 5f5862a..6787cff 100644
--- a/test/unit/response/getinfo.py
+++ b/test/unit/response/getinfo.py
@@ -56,7 +56,7 @@ class TestGetInfoResponse(unittest.TestCase):
self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
self.assertTrue(isinstance(control_message, stem.response.getinfo.GetInfoResponse))
- self.assertEqual({}, control_message.values)
+ self.assertEqual({}, control_message.entries)
def test_single_response(self):
"""
@@ -65,7 +65,7 @@ class TestGetInfoResponse(unittest.TestCase):
control_message = mocking.get_message(SINGLE_RESPONSE)
stem.response.convert("GETINFO", control_message)
- self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.values)
+ self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.entries)
def test_batch_response(self):
"""
@@ -81,7 +81,7 @@ class TestGetInfoResponse(unittest.TestCase):
"fingerprint": "5FDE0422045DF0E1879A3738D09099EB4A0C5BA0",
}
- self.assertEqual(expected, control_message.values)
+ self.assertEqual(expected, control_message.entries)
def test_multiline_response(self):
"""
@@ -97,7 +97,7 @@ class TestGetInfoResponse(unittest.TestCase):
"config-text": "\n".join(MULTILINE_RESPONSE.splitlines()[2:8]),
}
- self.assertEqual(expected, control_message.values)
+ self.assertEqual(expected, control_message.entries)
def test_invalid_non_mapping_content(self):
"""
1
0
commit d3fe6a1ed751a60b395f80aa7b4663ab6f78d230
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 16:38:30 2012 -0700
Cleaning up response classes
General refactoring of the response classes.
---
stem/connection.py | 28 ++++++------
stem/response/__init__.py | 2 +-
stem/response/getinfo.py | 19 ++++----
stem/response/protocolinfo.py | 81 +++++++++++++++++------------------
test/integ/response/protocolinfo.py | 6 +-
test/unit/response/protocolinfo.py | 7 +++-
6 files changed, 72 insertions(+), 71 deletions(-)
diff --git a/stem/connection.py b/stem/connection.py
index 34c9c68..c48a7b5 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -82,21 +82,7 @@ import stem.version
import stem.util.enum
import stem.util.system
import stem.util.log as log
-
-# Methods by which a controller can authenticate to the control port. Tor gives
-# a list of all the authentication methods it will accept in response to
-# PROTOCOLINFO queries.
-#
-# NONE - No authentication required
-# PASSWORD - See tor's HashedControlPassword option. Controllers must provide
-# the password used to generate the hash.
-# COOKIE - See tor's CookieAuthentication option. Controllers need to supply
-# the contents of the cookie file.
-# UNKNOWN - Tor provided one or more authentication methods that we don't
-# recognize. This is probably from a new addition to the control
-# protocol.
-
-AuthMethod = stem.util.enum.Enum("NONE", "PASSWORD", "COOKIE", "UNKNOWN")
+from stem.response.protocolinfo import AuthMethod
class AuthenticationFailure(Exception):
"""
@@ -628,6 +614,13 @@ def get_protocolinfo(controller):
the tor process running on it. If the socket is already closed then it is
first reconnected.
+ According to the control spec the cookie_file is an absolute path. However,
+ this often is not the case (especially for the Tor Browser Bundle)...
+ https://trac.torproject.org/projects/tor/ticket/1101
+
+ If the path is relative then we'll make an attempt (which may not work) to
+ correct this.
+
Arguments:
controller (stem.socket.ControlSocket or stem.control.BaseController) -
tor controller connection
@@ -659,6 +652,11 @@ def get_protocolinfo(controller):
stem.response.convert("PROTOCOLINFO", protocolinfo_response)
+ # attempt to expand relative cookie paths
+
+ if protocolinfo_response.cookie_path:
+ stem.connection._expand_cookie_path(protocolinfo_response, stem.util.system.get_pid_by_name, "tor")
+
# attempt to expand relative cookie paths via the control port or socket file
if isinstance(controller, stem.socket.ControlSocket):
diff --git a/stem/response/__init__.py b/stem/response/__init__.py
index 92b9846..ab38747 100644
--- a/stem/response/__init__.py
+++ b/stem/response/__init__.py
@@ -1,7 +1,7 @@
"""
Parses replies from the control socket.
-converts - translates a ControlMessage into a particular response subclass
+convert - translates a ControlMessage into a particular response subclass
"""
__all__ = ["getinfo", "protocolinfo"]
diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py
index dabbe12..620ca02 100644
--- a/stem/response/getinfo.py
+++ b/stem/response/getinfo.py
@@ -22,26 +22,25 @@ class GetInfoResponse(stem.socket.ControlMessage):
# 250 OK
self.entries = {}
+ remaining_lines = list(self)
- lines = list(self)
-
- if not self.is_ok() or not lines.pop() == "OK":
+ if not self.is_ok() or not remaining_lines.pop() == "OK":
raise stem.socket.ProtocolError("GETINFO response didn't have an OK status:\n%s" % self)
- for line in lines:
- if not "=" in line:
+ while remaining_lines:
+ try:
+ key, value = remaining_lines.pop(0).split("=", 1)
+ except ValueError:
raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings:\n%s" % self)
- key, value = line.split("=", 1)
-
# if the value is a multiline value then it *must* be of the form
# '<key>=\n<value>'
if "\n" in value:
- if value.startswith("\n"):
- value = value[1:]
- else:
+ if not value.startswith("\n"):
raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline:\n%s" % self)
+
+ value = value[1:]
self.entries[key] = value
diff --git a/stem/response/protocolinfo.py b/stem/response/protocolinfo.py
index 97ecfb7..7ead680 100644
--- a/stem/response/protocolinfo.py
+++ b/stem/response/protocolinfo.py
@@ -1,19 +1,27 @@
-import stem.connection
import stem.socket
import stem.version
+import stem.util.enum
import stem.util.log as log
+# Methods by which a controller can authenticate to the control port. Tor gives
+# a list of all the authentication methods it will accept in response to
+# PROTOCOLINFO queries.
+#
+# NONE - No authentication required
+# PASSWORD - See tor's HashedControlPassword option. Controllers must provide
+# the password used to generate the hash.
+# COOKIE - See tor's CookieAuthentication option. Controllers need to supply
+# the contents of the cookie file.
+# UNKNOWN - Tor provided one or more authentication methods that we don't
+# recognize. This is probably from a new addition to the control
+# protocol.
+
+AuthMethod = stem.util.enum.Enum("NONE", "PASSWORD", "COOKIE", "UNKNOWN")
+
class ProtocolInfoResponse(stem.socket.ControlMessage):
"""
Version one PROTOCOLINFO query response.
- According to the control spec the cookie_file is an absolute path. However,
- this often is not the case (especially for the Tor Browser Bundle)...
- https://trac.torproject.org/projects/tor/ticket/1101
-
- If the path is relative then we'll make an attempt (which may not work) to
- correct this.
-
The protocol_version is the only mandatory data for a valid PROTOCOLINFO
response, so all other values are None if undefined or empty if a collection.
@@ -34,20 +42,22 @@ class ProtocolInfoResponse(stem.socket.ControlMessage):
self.protocol_version = None
self.tor_version = None
+ self.auth_methods = ()
+ self.unknown_auth_methods = ()
self.cookie_path = None
auth_methods, unknown_auth_methods = [], []
- lines = list(self)
+ remaining_lines = list(self)
- if not self.is_ok() or not lines.pop() == "OK":
- raise stem.socket.ProtocolError("GETINFO response didn't have an OK status:\n%s" % self)
+ if not self.is_ok() or not remaining_lines.pop() == "OK":
+ raise stem.socket.ProtocolError("PROTOCOLINFO response didn't have an OK status:\n%s" % self)
# sanity check that we're a PROTOCOLINFO response
- if not lines[0].startswith("PROTOCOLINFO"):
- msg = "Message is not a PROTOCOLINFO response (%s)" % self
- raise stem.socket.ProtocolError(msg)
+ if not remaining_lines[0].startswith("PROTOCOLINFO"):
+ raise stem.socket.ProtocolError("Message is not a PROTOCOLINFO response:\n%s" % self)
- for line in lines:
+ while remaining_lines:
+ line = remaining_lines.pop(0)
line_type = line.pop()
if line_type == "PROTOCOLINFO":
@@ -56,16 +66,12 @@ class ProtocolInfoResponse(stem.socket.ControlMessage):
# PIVERSION = 1*DIGIT
if line.is_empty():
- msg = "PROTOCOLINFO response's initial line is missing the protocol version: %s" % line
- raise stem.socket.ProtocolError(msg)
+ raise stem.socket.ProtocolError("PROTOCOLINFO response's initial line is missing the protocol version: %s" % line)
- piversion = line.pop()
-
- if not piversion.isdigit():
- msg = "PROTOCOLINFO response version is non-numeric: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- self.protocol_version = int(piversion)
+ try:
+ self.protocol_version = int(line.pop())
+ except ValueError:
+ raise stem.socket.ProtocolError("PROTOCOLINFO response version is non-numeric: %s" % line)
# The piversion really should be "1" but, according to the spec, tor
# does not necessarily need to provide the PROTOCOLINFO version that we
@@ -83,49 +89,42 @@ class ProtocolInfoResponse(stem.socket.ControlMessage):
# parse AuthMethod mapping
if not line.is_next_mapping("METHODS"):
- msg = "PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line
- raise stem.socket.ProtocolError(msg)
+ raise stem.socket.ProtocolError("PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line)
for method in line.pop_mapping()[1].split(","):
if method == "NULL":
- auth_methods.append(stem.connection.AuthMethod.NONE)
+ auth_methods.append(AuthMethod.NONE)
elif method == "HASHEDPASSWORD":
- auth_methods.append(stem.connection.AuthMethod.PASSWORD)
+ auth_methods.append(AuthMethod.PASSWORD)
elif method == "COOKIE":
- auth_methods.append(stem.connection.AuthMethod.COOKIE)
+ auth_methods.append(AuthMethod.COOKIE)
else:
unknown_auth_methods.append(method)
- message_id = "stem.connection.unknown_auth_%s" % method
+ message_id = "stem.response.protocolinfo.unknown_auth_%s" % method
log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %s" % method)
# our auth_methods should have a single AuthMethod.UNKNOWN entry if
# any unknown authentication methods exist
- if not stem.connection.AuthMethod.UNKNOWN in auth_methods:
- auth_methods.append(stem.connection.AuthMethod.UNKNOWN)
+ if not AuthMethod.UNKNOWN in auth_methods:
+ auth_methods.append(AuthMethod.UNKNOWN)
# parse optional COOKIEFILE mapping (quoted and can have escapes)
if line.is_next_mapping("COOKIEFILE", True, True):
self.cookie_path = line.pop_mapping(True, True)[1]
-
- # attempt to expand relative cookie paths
- stem.connection._expand_cookie_path(self, stem.util.system.get_pid_by_name, "tor")
elif line_type == "VERSION":
# Line format:
# VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF
# TorVersion = QuotedString
if not line.is_next_mapping("Tor", True):
- msg = "PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line
- raise stem.socket.ProtocolError(msg)
-
- torversion = line.pop_mapping(True)[1]
+ raise stem.socket.ProtocolError("PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line)
try:
- self.tor_version = stem.version.Version(torversion)
+ self.tor_version = stem.version.Version(line.pop_mapping(True)[1])
except ValueError, exc:
raise stem.socket.ProtocolError(exc)
else:
- log.debug("unrecognized PROTOCOLINFO line type '%s', ignoring entry: %s" % (line_type, line))
+ log.debug("Unrecognized PROTOCOLINFO line type '%s', ignoring it: %s" % (line_type, line))
self.auth_methods = tuple(auth_methods)
self.unknown_auth_methods = tuple(unknown_auth_methods)
diff --git a/test/integ/response/protocolinfo.py b/test/integ/response/protocolinfo.py
index f5eb518..e8e8c12 100644
--- a/test/integ/response/protocolinfo.py
+++ b/test/integ/response/protocolinfo.py
@@ -119,7 +119,7 @@ class TestProtocolInfo(unittest.TestCase):
auth_methods, auth_cookie_path = [], None
if test.runner.Torrc.COOKIE in tor_options:
- auth_methods.append(stem.connection.AuthMethod.COOKIE)
+ auth_methods.append(stem.response.protocolinfo.AuthMethod.COOKIE)
chroot_path = runner.get_chroot()
auth_cookie_path = runner.get_auth_cookie_path()
@@ -127,10 +127,10 @@ class TestProtocolInfo(unittest.TestCase):
auth_cookie_path = auth_cookie_path[len(chroot_path):]
if test.runner.Torrc.PASSWORD in tor_options:
- auth_methods.append(stem.connection.AuthMethod.PASSWORD)
+ auth_methods.append(stem.response.protocolinfo.AuthMethod.PASSWORD)
if not auth_methods:
- auth_methods.append(stem.connection.AuthMethod.NONE)
+ auth_methods.append(stem.response.protocolinfo.AuthMethod.NONE)
self.assertEqual((), protocolinfo_response.unknown_auth_methods)
self.assertEqual(tuple(auth_methods), protocolinfo_response.auth_methods)
diff --git a/test/unit/response/protocolinfo.py b/test/unit/response/protocolinfo.py
index a17c58c..1d282ea 100644
--- a/test/unit/response/protocolinfo.py
+++ b/test/unit/response/protocolinfo.py
@@ -4,7 +4,6 @@ Unit tests for the stem.response.protocolinfo.ProtocolInfoResponse class.
import unittest
-from stem.connection import AuthMethod
import stem.socket
import stem.version
import stem.util.proc
@@ -12,6 +11,7 @@ import stem.util.system
import stem.response
import stem.response.protocolinfo
import test.mocking as mocking
+from stem.response.protocolinfo import AuthMethod
NO_AUTH = """250-PROTOCOLINFO 1
250-AUTH METHODS=NULL
@@ -148,6 +148,8 @@ class TestProtocolInfoResponse(unittest.TestCase):
succeeds and fails.
"""
+ # TODO: move into stem.connection unit tests?
+
# we need to mock both pid and cwd lookups since the general cookie
# expanion works by...
# - resolving the pid of the "tor" process
@@ -165,6 +167,9 @@ class TestProtocolInfoResponse(unittest.TestCase):
control_message = mocking.get_message(RELATIVE_COOKIE_PATH)
stem.response.convert("PROTOCOLINFO", control_message)
+
+ stem.connection._expand_cookie_path(control_message, stem.util.system.get_pid_by_name, "tor")
+
self.assertEquals("/tmp/foo/tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
# exercise cookie expansion where both calls fail (should work, just
1
0

[stem/master] Moving ControlMessage and ControlLine into stem.response
by atagar@torproject.org 29 May '12
by atagar@torproject.org 29 May '12
29 May '12
commit 2a21ec5848df9d1d5a520abbb6d5927a804acd09
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon May 28 17:07:57 2012 -0700
Moving ControlMessage and ControlLine into stem.response
The ControlMessage and the ControlLine instances in it are the... well,
messages that we get from tor. It belongs in stem.response so moving it there.
Functionally this is fine, and I'm happy with the tests in a functional fashion
as well. However, all this splitting and refactoring has made the tests a mess
in terms of what the test functions belong to (some test.unit.response tests
are checking stem.connection functionality, for instance). More cleanup work to
do there...
---
run_tests.py | 8 +-
stem/control.py | 11 +-
stem/response/__init__.py | 359 ++++++++++++++++++++++++++++++++-
stem/response/getinfo.py | 3 +-
stem/response/protocolinfo.py | 3 +-
stem/socket.py | 353 +--------------------------------
test/integ/socket/control_message.py | 2 +-
test/unit/response/__init__.py | 2 +-
test/unit/response/control_line.py | 166 +++++++++++++++
test/unit/response/control_message.py | 184 +++++++++++++++++
test/unit/response/getinfo.py | 3 +-
test/unit/response/protocolinfo.py | 2 +-
test/unit/socket/__init__.py | 6 -
test/unit/socket/control_line.py | 166 ---------------
test/unit/socket/control_message.py | 184 -----------------
15 files changed, 723 insertions(+), 729 deletions(-)
diff --git a/run_tests.py b/run_tests.py
index dea86c3..22adce3 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -16,11 +16,11 @@ import test.output
import test.runner
import test.check_whitespace
import test.unit.connection.authentication
-import test.unit.socket.control_line
-import test.unit.socket.control_message
import test.unit.descriptor.reader
import test.unit.descriptor.server_descriptor
import test.unit.descriptor.extrainfo_descriptor
+import test.unit.response.control_line
+import test.unit.response.control_message
import test.unit.response.getinfo
import test.unit.response.protocolinfo
import test.unit.util.conf
@@ -100,10 +100,10 @@ UNIT_TESTS = (
test.unit.descriptor.server_descriptor.TestServerDescriptor,
test.unit.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor,
test.unit.version.TestVersion,
+ test.unit.response.control_message.TestControlMessage,
+ test.unit.response.control_line.TestControlLine,
test.unit.response.getinfo.TestGetInfoResponse,
test.unit.response.protocolinfo.TestProtocolInfoResponse,
- test.unit.socket.control_message.TestControlMessage,
- test.unit.socket.control_line.TestControlLine,
test.unit.connection.authentication.TestAuthenticate,
)
diff --git a/stem/control.py b/stem/control.py
index 1f3b767..a6bc3d2 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -92,7 +92,7 @@ class BaseController:
message (str) - message to be formatted and sent to tor
Returns:
- stem.socket.ControlMessage with the response
+ stem.response.ControlMessage with the response
Raises:
stem.socket.ProtocolError the content from the socket is malformed
@@ -128,7 +128,7 @@ class BaseController:
log.info("Tor provided a malformed message (%s)" % response)
elif isinstance(response, stem.socket.ControllerError):
log.info("Socket experienced a problem (%s)" % response)
- elif isinstance(response, stem.socket.ControlMessage):
+ elif isinstance(response, stem.response.ControlMessage):
log.notice("BUG: the msg() function failed to deliver a response: %s" % response)
except Queue.Empty:
# the empty() method is documented to not be fully reliable so this
@@ -260,7 +260,7 @@ class BaseController:
notified whenever we receive an event from the control socket.
Arguments:
- event_message (stem.socket.ControlMessage) - message received from the
+ event_message (stem.response.ControlMessage) - message received from the
control socket
"""
@@ -471,11 +471,6 @@ class Controller(BaseController):
try:
response = self.msg("GETINFO %s" % " ".join(param))
-
- # TODO: replace with is_ok() check when we've merged it in
- if response.content()[0][0] != "250":
- raise stem.socket.ControllerError(str(response))
-
stem.response.convert("GETINFO", response)
# error if we got back different parameters than we requested
diff --git a/stem/response/__init__.py b/stem/response/__init__.py
index ab38747..1f2ed0c 100644
--- a/stem/response/__init__.py
+++ b/stem/response/__init__.py
@@ -2,11 +2,39 @@
Parses replies from the control socket.
convert - translates a ControlMessage into a particular response subclass
+
+ControlMessage - Message that's read from the control socket.
+ |- content - provides the parsed message content
+ |- raw_content - unparsed socket data
+ |- __str__ - content stripped of protocol formatting
+ +- __iter__ - ControlLine entries for the content of the message
+
+ControlLine - String subclass with methods for parsing controller responses.
+ |- remainder - provides the unparsed content
+ |- is_empty - checks if the remaining content is empty
+ |- is_next_quoted - checks if the next entry is a quoted value
+ |- is_next_mapping - checks if the next entry is a KEY=VALUE mapping
+ |- peek_key - provides the key of the next entry
+ |- pop - removes and returns the next entry
+ +- pop_mapping - removes and returns the next entry as a KEY=VALUE mapping
"""
-__all__ = ["getinfo", "protocolinfo"]
+__all__ = ["getinfo", "protocolinfo", "convert", "ControlMessage", "ControlLine"]
+
+import re
+import threading
+
+KEY_ARG = re.compile("^(\S+)=")
-import stem.socket
+# Escape sequences from the 'esc_for_log' function of tor's 'common/util.c'.
+# It's hard to tell what controller functions use this in practice, but direct
+# users are...
+# - 'COOKIEFILE' field of PROTOCOLINFO responses
+# - logged messages about bugs
+# - the 'getinfo_helper_listeners' function of control.c
+
+CONTROL_ESCAPES = {r"\\": "\\", r"\"": "\"", r"\'": "'",
+ r"\r": "\r", r"\n": "\n", r"\t": "\t"}
def convert(response_type, message):
"""
@@ -20,8 +48,8 @@ def convert(response_type, message):
If the response_type isn't recognized then this is leaves it alone.
Arguments:
- response_type (str) - type of tor response to convert to
- message (stem.socket.ControlMessage) - message to be converted
+ response_type (str) - type of tor response to convert to
+ message (stem.response.ControlMessage) - message to be converted
Raises:
stem.socket.ProtocolError the message isn't a proper response of that type
@@ -32,8 +60,8 @@ def convert(response_type, message):
import stem.response.getinfo
import stem.response.protocolinfo
- if not isinstance(message, stem.socket.ControlMessage):
- raise TypeError("Only able to convert stem.socket.ControlMessage instances")
+ if not isinstance(message, ControlMessage):
+ raise TypeError("Only able to convert stem.response.ControlMessage instances")
if response_type == "GETINFO":
response_class = stem.response.getinfo.GetInfoResponse
@@ -44,3 +72,322 @@ def convert(response_type, message):
message.__class__ = response_class
message._parse_message()
+class ControlMessage:
+ """
+ Message from the control socket. This is iterable and can be stringified for
+ individual message components stripped of protocol formatting.
+ """
+
+ def __init__(self, parsed_content, raw_content):
+ self._parsed_content = parsed_content
+ self._raw_content = raw_content
+
+ def is_ok(self):
+ """
+ Checks if all of our lines have a 250 response.
+
+ Returns:
+ True if all lines have a 250 response code, False otherwise
+ """
+
+ for code, _, _ in self._parsed_content:
+ if code != "250": return False
+
+ return True
+
+ def content(self):
+ """
+ Provides the parsed message content. These are entries of the form...
+ (status_code, divider, content)
+
+ * status_code - Three character code for the type of response (defined in
+ section 4 of the control-spec).
+ * divider - Single character to indicate if this is mid-reply, data, or
+ an end to the message (defined in section 2.3 of the
+ control-spec).
+ * content - The following content is the actual payload of the line.
+
+ For data entries the content is the full multi-line payload with newline
+ linebreaks and leading periods unescaped.
+
+ Returns:
+ list of (str, str, str) tuples for the components of this message
+ """
+
+ return list(self._parsed_content)
+
+ def raw_content(self):
+ """
+ Provides the unparsed content read from the control socket.
+
+ Returns:
+ string of the socket data used to generate this message
+ """
+
+ return self._raw_content
+
+ def __str__(self):
+ """
+ Content of the message, stripped of status code and divider protocol
+ formatting.
+ """
+
+ return "\n".join(list(self))
+
+ def __iter__(self):
+ """
+ Provides ControlLine instances for the content of the message. This is
+ stripped of status codes and dividers, for instance...
+
+ 250+info/names=
+ desc/id/* -- Router descriptors by ID.
+ desc/name/* -- Router descriptors by nickname.
+ .
+ 250 OK
+
+ Would provide two entries...
+ 1st - "info/names=
+ desc/id/* -- Router descriptors by ID.
+ desc/name/* -- Router descriptors by nickname."
+ 2nd - "OK"
+ """
+
+ for _, _, content in self._parsed_content:
+ yield ControlLine(content)
+
+class ControlLine(str):
+ """
+ String subclass that represents a line of controller output. This behaves as
+ a normal string with additional methods for parsing and popping entries from
+ a space delimited series of elements like a stack.
+
+ None of these additional methods effect ourselves as a string (which is still
+ immutable). All methods are thread safe.
+ """
+
+ def __new__(self, value):
+ return str.__new__(self, value)
+
+ def __init__(self, value):
+ self._remainder = value
+ self._remainder_lock = threading.RLock()
+
+ def remainder(self):
+ """
+ Provides our unparsed content. This is an empty string after we've popped
+ all entries.
+
+ Returns:
+ str of the unparsed content
+ """
+
+ return self._remainder
+
+ def is_empty(self):
+ """
+ Checks if we have further content to pop or not.
+
+ Returns:
+ True if we have additional content, False otherwise
+ """
+
+ return self._remainder == ""
+
+ def is_next_quoted(self, escaped = False):
+ """
+ Checks if our next entry is a quoted value or not.
+
+ Arguments:
+ escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
+
+ Returns:
+ True if the next entry can be parsed as a quoted value, False otherwise
+ """
+
+ start_quote, end_quote = _get_quote_indeces(self._remainder, escaped)
+ return start_quote == 0 and end_quote != -1
+
+ def is_next_mapping(self, key = None, quoted = False, escaped = False):
+ """
+ Checks if our next entry is a KEY=VALUE mapping or not.
+
+ Arguments:
+ key (str) - checks that the key matches this value, skipping the
+ check if None
+ quoted (bool) - checks that the mapping is to a quoted value
+ escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
+
+ Returns:
+ True if the next entry can be parsed as a key=value mapping, False
+ otherwise
+ """
+
+ remainder = self._remainder # temp copy to avoid locking
+ key_match = KEY_ARG.match(remainder)
+
+ if key_match:
+ if key and key != key_match.groups()[0]:
+ return False
+
+ if quoted:
+ # checks that we have a quoted value and that it comes after the 'key='
+ start_quote, end_quote = _get_quote_indeces(remainder, escaped)
+ return start_quote == key_match.end() and end_quote != -1
+ else:
+ return True # we just needed to check for the key
+ else:
+ return False # doesn't start with a key
+
+ def peek_key(self):
+ """
+ Provides the key of the next entry, providing None if it isn't a key/value
+ mapping.
+
+ Returns:
+ str with the next entry's key
+ """
+
+ remainder = self._remainder
+ key_match = KEY_ARG.match(remainder)
+
+ if key_match:
+ return key_match.groups()[0]
+ else:
+ return None
+
+ def pop(self, quoted = False, escaped = False):
+ """
+ Parses the next space separated entry, removing it and the space from our
+ remaining content. Examples...
+
+ >>> line = ControlLine("\"We're all mad here.\" says the grinning cat.")
+ >>> print line.pop(True)
+ "We're all mad here."
+ >>> print line.pop()
+ "says"
+ >>> print line.remainder()
+ "the grinning cat."
+
+ >>> line = ControlLine("\"this has a \\\" and \\\\ in it\" foo=bar more_data")
+ >>> print line.pop(True, True)
+ "this has a \" and \\ in it"
+
+ Arguments:
+ quoted (bool) - parses the next entry as a quoted value, removing the
+ quotes
+ escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
+
+ Returns:
+ str of the next space separated entry
+
+ Raises:
+ ValueError if quoted is True without the value being quoted
+ IndexError if we don't have any remaining content left to parse
+ """
+
+ with self._remainder_lock:
+ next_entry, remainder = _parse_entry(self._remainder, quoted, escaped)
+ self._remainder = remainder
+ return next_entry
+
+ def pop_mapping(self, quoted = False, escaped = False):
+ """
+ Parses the next space separated entry as a KEY=VALUE mapping, removing it
+ and the space from our remaining content.
+
+ Arguments:
+ quoted (bool) - parses the value as being quoted, removing the quotes
+ escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
+
+ Returns:
+ tuple of the form (key, value)
+
+ Raises:
+ ValueError if this isn't a KEY=VALUE mapping or if quoted is True without
+ the value being quoted
+ """
+
+ with self._remainder_lock:
+ if self.is_empty(): raise IndexError("no remaining content to parse")
+ key_match = KEY_ARG.match(self._remainder)
+
+ if not key_match:
+ raise ValueError("the next entry isn't a KEY=VALUE mapping: " + self._remainder)
+
+ # parse off the key
+ key = key_match.groups()[0]
+ remainder = self._remainder[key_match.end():]
+
+ next_entry, remainder = _parse_entry(remainder, quoted, escaped)
+ self._remainder = remainder
+ return (key, next_entry)
+
+def _parse_entry(line, quoted, escaped):
+ """
+ Parses the next entry from the given space separated content.
+
+ Arguments:
+ line (str) - content to be parsed
+ quoted (bool) - parses the next entry as a quoted value, removing the
+ quotes
+ escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
+
+ Returns:
+ tuple of the form (entry, remainder)
+
+ Raises:
+ ValueError if quoted is True without the next value being quoted
+ IndexError if there's nothing to parse from the line
+ """
+
+ if line == "":
+ raise IndexError("no remaining content to parse")
+
+ next_entry, remainder = "", line
+
+ if quoted:
+ # validate and parse the quoted value
+ start_quote, end_quote = _get_quote_indeces(remainder, escaped)
+
+ if start_quote != 0 or end_quote == -1:
+ raise ValueError("the next entry isn't a quoted value: " + line)
+
+ next_entry, remainder = remainder[1 : end_quote], remainder[end_quote + 1:]
+ else:
+ # non-quoted value, just need to check if there's more data afterward
+ if " " in remainder: next_entry, remainder = remainder.split(" ", 1)
+ else: next_entry, remainder = remainder, ""
+
+ if escaped:
+ for esc_sequence, replacement in CONTROL_ESCAPES.items():
+ next_entry = next_entry.replace(esc_sequence, replacement)
+
+ return (next_entry, remainder.lstrip())
+
+def _get_quote_indeces(line, escaped):
+ """
+ Provides the indices of the next two quotes in the given content.
+
+ Arguments:
+ line (str) - content to be parsed
+ escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
+
+ Returns:
+ tuple of two ints, indices being -1 if a quote doesn't exist
+ """
+
+ indices, quote_index = [], -1
+
+ for _ in range(2):
+ quote_index = line.find("\"", quote_index + 1)
+
+ # if we have escapes then we need to skip any r'\"' entries
+ if escaped:
+ # skip check if index is -1 (no match) or 0 (first character)
+ while quote_index >= 1 and line[quote_index - 1] == "\\":
+ quote_index = line.find("\"", quote_index + 1)
+
+ indices.append(quote_index)
+
+ return tuple(indices)
+
diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py
index 620ca02..6f6cde4 100644
--- a/stem/response/getinfo.py
+++ b/stem/response/getinfo.py
@@ -1,6 +1,7 @@
import stem.socket
+import stem.response
-class GetInfoResponse(stem.socket.ControlMessage):
+class GetInfoResponse(stem.response.ControlMessage):
"""
Reply for a GETINFO query.
diff --git a/stem/response/protocolinfo.py b/stem/response/protocolinfo.py
index 7ead680..ce9a82c 100644
--- a/stem/response/protocolinfo.py
+++ b/stem/response/protocolinfo.py
@@ -1,4 +1,5 @@
import stem.socket
+import stem.response
import stem.version
import stem.util.enum
import stem.util.log as log
@@ -18,7 +19,7 @@ import stem.util.log as log
AuthMethod = stem.util.enum.Enum("NONE", "PASSWORD", "COOKIE", "UNKNOWN")
-class ProtocolInfoResponse(stem.socket.ControlMessage):
+class ProtocolInfoResponse(stem.response.ControlMessage):
"""
Version one PROTOCOLINFO query response.
diff --git a/stem/socket.py b/stem/socket.py
index a9e2068..68cc7e4 100644
--- a/stem/socket.py
+++ b/stem/socket.py
@@ -18,21 +18,6 @@ ControlSocket - Socket wrapper that speaks the tor control protocol.
|- close - shuts down the socket
+- __enter__ / __exit__ - manages socket connection
-ControlMessage - Message that's read from the control socket.
- |- content - provides the parsed message content
- |- raw_content - unparsed socket data
- |- __str__ - content stripped of protocol formatting
- +- __iter__ - ControlLine entries for the content of the message
-
-ControlLine - String subclass with methods for parsing controller responses.
- |- remainder - provides the unparsed content
- |- is_empty - checks if the remaining content is empty
- |- is_next_quoted - checks if the next entry is a quoted value
- |- is_next_mapping - checks if the next entry is a KEY=VALUE mapping
- |- peek_key - provides the key of the next entry
- |- pop - removes and returns the next entry
- +- pop_mapping - removes and returns the next entry as a KEY=VALUE mapping
-
send_message - Writes a message to a control socket.
recv_message - Reads a ControlMessage from a control socket.
send_formatting - Performs the formatting expected from sent messages.
@@ -48,21 +33,10 @@ import re
import socket
import threading
+import stem.response
import stem.util.enum
import stem.util.log as log
-KEY_ARG = re.compile("^(\S+)=")
-
-# Escape sequences from the 'esc_for_log' function of tor's 'common/util.c'.
-# It's hard to tell what controller functions use this in practice, but direct
-# users are...
-# - 'COOKIEFILE' field of PROTOCOLINFO responses
-# - logged messages about bugs
-# - the 'getinfo_helper_listeners' function of control.c
-
-CONTROL_ESCAPES = {r"\\": "\\", r"\"": "\"", r"\'": "'",
- r"\r": "\r", r"\n": "\n", r"\t": "\t"}
-
class ControllerError(Exception):
"Base error for controller communication issues."
@@ -127,7 +101,7 @@ class ControlSocket:
one. For more information see the stem.socket.recv_message function.
Returns:
- stem.socket.ControlMessage for the message received
+ stem.response.ControlMessage for the message received
Raises:
stem.socket.ProtocolError the content from the socket is malformed
@@ -398,325 +372,6 @@ class ControlSocketFile(ControlSocket):
except socket.error, exc:
raise SocketError(exc)
-class ControlMessage:
- """
- Message from the control socket. This is iterable and can be stringified for
- individual message components stripped of protocol formatting.
- """
-
- def __init__(self, parsed_content, raw_content):
- self._parsed_content = parsed_content
- self._raw_content = raw_content
-
- def is_ok(self):
- """
- Checks if all of our lines have a 250 response.
-
- Returns:
- True if all lines have a 250 response code, False otherwise
- """
-
- for code, _, _ in self._parsed_content:
- if code != "250": return False
-
- return True
-
- def content(self):
- """
- Provides the parsed message content. These are entries of the form...
- (status_code, divider, content)
-
- * status_code - Three character code for the type of response (defined in
- section 4 of the control-spec).
- * divider - Single character to indicate if this is mid-reply, data, or
- an end to the message (defined in section 2.3 of the
- control-spec).
- * content - The following content is the actual payload of the line.
-
- For data entries the content is the full multi-line payload with newline
- linebreaks and leading periods unescaped.
-
- Returns:
- list of (str, str, str) tuples for the components of this message
- """
-
- return list(self._parsed_content)
-
- def raw_content(self):
- """
- Provides the unparsed content read from the control socket.
-
- Returns:
- string of the socket data used to generate this message
- """
-
- return self._raw_content
-
- def __str__(self):
- """
- Content of the message, stripped of status code and divider protocol
- formatting.
- """
-
- return "\n".join(list(self))
-
- def __iter__(self):
- """
- Provides ControlLine instances for the content of the message. This is
- stripped of status codes and dividers, for instance...
-
- 250+info/names=
- desc/id/* -- Router descriptors by ID.
- desc/name/* -- Router descriptors by nickname.
- .
- 250 OK
-
- Would provide two entries...
- 1st - "info/names=
- desc/id/* -- Router descriptors by ID.
- desc/name/* -- Router descriptors by nickname."
- 2nd - "OK"
- """
-
- for _, _, content in self._parsed_content:
- yield ControlLine(content)
-
-class ControlLine(str):
- """
- String subclass that represents a line of controller output. This behaves as
- a normal string with additional methods for parsing and popping entries from
- a space delimited series of elements like a stack.
-
- None of these additional methods effect ourselves as a string (which is still
- immutable). All methods are thread safe.
- """
-
- def __new__(self, value):
- return str.__new__(self, value)
-
- def __init__(self, value):
- self._remainder = value
- self._remainder_lock = threading.RLock()
-
- def remainder(self):
- """
- Provides our unparsed content. This is an empty string after we've popped
- all entries.
-
- Returns:
- str of the unparsed content
- """
-
- return self._remainder
-
- def is_empty(self):
- """
- Checks if we have further content to pop or not.
-
- Returns:
- True if we have additional content, False otherwise
- """
-
- return self._remainder == ""
-
- def is_next_quoted(self, escaped = False):
- """
- Checks if our next entry is a quoted value or not.
-
- Arguments:
- escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
-
- Returns:
- True if the next entry can be parsed as a quoted value, False otherwise
- """
-
- start_quote, end_quote = _get_quote_indeces(self._remainder, escaped)
- return start_quote == 0 and end_quote != -1
-
- def is_next_mapping(self, key = None, quoted = False, escaped = False):
- """
- Checks if our next entry is a KEY=VALUE mapping or not.
-
- Arguments:
- key (str) - checks that the key matches this value, skipping the
- check if None
- quoted (bool) - checks that the mapping is to a quoted value
- escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
-
- Returns:
- True if the next entry can be parsed as a key=value mapping, False
- otherwise
- """
-
- remainder = self._remainder # temp copy to avoid locking
- key_match = KEY_ARG.match(remainder)
-
- if key_match:
- if key and key != key_match.groups()[0]:
- return False
-
- if quoted:
- # checks that we have a quoted value and that it comes after the 'key='
- start_quote, end_quote = _get_quote_indeces(remainder, escaped)
- return start_quote == key_match.end() and end_quote != -1
- else:
- return True # we just needed to check for the key
- else:
- return False # doesn't start with a key
-
- def peek_key(self):
- """
- Provides the key of the next entry, providing None if it isn't a key/value
- mapping.
-
- Returns:
- str with the next entry's key
- """
-
- remainder = self._remainder
- key_match = KEY_ARG.match(remainder)
-
- if key_match:
- return key_match.groups()[0]
- else:
- return None
-
- def pop(self, quoted = False, escaped = False):
- """
- Parses the next space separated entry, removing it and the space from our
- remaining content. Examples...
-
- >>> line = ControlLine("\"We're all mad here.\" says the grinning cat.")
- >>> print line.pop(True)
- "We're all mad here."
- >>> print line.pop()
- "says"
- >>> print line.remainder()
- "the grinning cat."
-
- >>> line = ControlLine("\"this has a \\\" and \\\\ in it\" foo=bar more_data")
- >>> print line.pop(True, True)
- "this has a \" and \\ in it"
-
- Arguments:
- quoted (bool) - parses the next entry as a quoted value, removing the
- quotes
- escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
-
- Returns:
- str of the next space separated entry
-
- Raises:
- ValueError if quoted is True without the value being quoted
- IndexError if we don't have any remaining content left to parse
- """
-
- with self._remainder_lock:
- next_entry, remainder = _parse_entry(self._remainder, quoted, escaped)
- self._remainder = remainder
- return next_entry
-
- def pop_mapping(self, quoted = False, escaped = False):
- """
- Parses the next space separated entry as a KEY=VALUE mapping, removing it
- and the space from our remaining content.
-
- Arguments:
- quoted (bool) - parses the value as being quoted, removing the quotes
- escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
-
- Returns:
- tuple of the form (key, value)
-
- Raises:
- ValueError if this isn't a KEY=VALUE mapping or if quoted is True without
- the value being quoted
- """
-
- with self._remainder_lock:
- if self.is_empty(): raise IndexError("no remaining content to parse")
- key_match = KEY_ARG.match(self._remainder)
-
- if not key_match:
- raise ValueError("the next entry isn't a KEY=VALUE mapping: " + self._remainder)
-
- # parse off the key
- key = key_match.groups()[0]
- remainder = self._remainder[key_match.end():]
-
- next_entry, remainder = _parse_entry(remainder, quoted, escaped)
- self._remainder = remainder
- return (key, next_entry)
-
-def _parse_entry(line, quoted, escaped):
- """
- Parses the next entry from the given space separated content.
-
- Arguments:
- line (str) - content to be parsed
- quoted (bool) - parses the next entry as a quoted value, removing the
- quotes
- escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
-
- Returns:
- tuple of the form (entry, remainder)
-
- Raises:
- ValueError if quoted is True without the next value being quoted
- IndexError if there's nothing to parse from the line
- """
-
- if line == "":
- raise IndexError("no remaining content to parse")
-
- next_entry, remainder = "", line
-
- if quoted:
- # validate and parse the quoted value
- start_quote, end_quote = _get_quote_indeces(remainder, escaped)
-
- if start_quote != 0 or end_quote == -1:
- raise ValueError("the next entry isn't a quoted value: " + line)
-
- next_entry, remainder = remainder[1 : end_quote], remainder[end_quote + 1:]
- else:
- # non-quoted value, just need to check if there's more data afterward
- if " " in remainder: next_entry, remainder = remainder.split(" ", 1)
- else: next_entry, remainder = remainder, ""
-
- if escaped:
- for esc_sequence, replacement in CONTROL_ESCAPES.items():
- next_entry = next_entry.replace(esc_sequence, replacement)
-
- return (next_entry, remainder.lstrip())
-
-def _get_quote_indeces(line, escaped):
- """
- Provides the indices of the next two quotes in the given content.
-
- Arguments:
- line (str) - content to be parsed
- escaped (bool) - unescapes the CONTROL_ESCAPES escape sequences
-
- Returns:
- tuple of two ints, indices being -1 if a quote doesn't exist
- """
-
- indices, quote_index = [], -1
-
- for _ in range(2):
- quote_index = line.find("\"", quote_index + 1)
-
- # if we have escapes then we need to skip any r'\"' entries
- if escaped:
- # skip check if index is -1 (no match) or 0 (first character)
- while quote_index >= 1 and line[quote_index - 1] == "\\":
- quote_index = line.find("\"", quote_index + 1)
-
- indices.append(quote_index)
-
- return tuple(indices)
-
def send_message(control_file, message, raw = False):
"""
Sends a message to the control socket, adding the expected formatting for
@@ -782,7 +437,7 @@ def recv_message(control_file):
socket's makefile() method for more information)
Returns:
- stem.socket.ControlMessage read from the socket
+ stem.response.ControlMessage read from the socket
Raises:
stem.socket.ProtocolError the content from the socket is malformed
@@ -848,7 +503,7 @@ def recv_message(control_file):
log_message = raw_content.replace("\r\n", "\n").rstrip()
log.trace("Received from tor:\n" + log_message)
- return ControlMessage(parsed_content, raw_content)
+ return stem.response.ControlMessage(parsed_content, raw_content)
elif divider == "+":
# data entry, all of the following lines belong to the content until we
# get a line with just a period
diff --git a/test/integ/socket/control_message.py b/test/integ/socket/control_message.py
index 72f9174..c148605 100644
--- a/test/integ/socket/control_message.py
+++ b/test/integ/socket/control_message.py
@@ -1,5 +1,5 @@
"""
-Integration tests for the stem.socket.ControlMessage class.
+Integration tests for the stem.response.ControlMessage class.
"""
import re
diff --git a/test/unit/response/__init__.py b/test/unit/response/__init__.py
index 530274c..c53d9ef 100644
--- a/test/unit/response/__init__.py
+++ b/test/unit/response/__init__.py
@@ -2,5 +2,5 @@
Unit tests for stem.response.
"""
-__all__ = ["getinfo", "protocolinfo"]
+__all__ = ["control_message", "control_line", "getinfo", "protocolinfo"]
diff --git a/test/unit/response/control_line.py b/test/unit/response/control_line.py
new file mode 100644
index 0000000..5240a46
--- /dev/null
+++ b/test/unit/response/control_line.py
@@ -0,0 +1,166 @@
+"""
+Unit tests for the stem.response.ControlLine class.
+"""
+
+import unittest
+import stem.response
+
+# response made by having 'DataDirectory /tmp/my data\"dir/' in the torrc
+PROTOCOLINFO_RESPONSE = (
+ 'PROTOCOLINFO 1',
+ 'AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\\\\\\"dir//control_auth_cookie"',
+ 'VERSION Tor="0.2.1.30"',
+ 'OK',
+)
+
+class TestControlLine(unittest.TestCase):
+ def test_pop_examples(self):
+ """
+ Checks that the pop method's pydoc examples are correct.
+ """
+
+ line = stem.response.ControlLine("\"We're all mad here.\" says the grinning cat.")
+ self.assertEquals(line.pop(True), "We're all mad here.")
+ self.assertEquals(line.pop(), "says")
+ self.assertEquals(line.remainder(), "the grinning cat.")
+
+ line = stem.response.ControlLine("\"this has a \\\" and \\\\ in it\" foo=bar more_data")
+ self.assertEquals(line.pop(True, True), "this has a \" and \\ in it")
+
+ def test_string(self):
+ """
+ Basic checks that we behave as a regular immutable string.
+ """
+
+ line = stem.response.ControlLine(PROTOCOLINFO_RESPONSE[0])
+ self.assertEquals(line, 'PROTOCOLINFO 1')
+ self.assertTrue(line.startswith('PROTOCOLINFO '))
+
+ # checks that popping items doesn't effect us
+ line.pop()
+ self.assertEquals(line, 'PROTOCOLINFO 1')
+ self.assertTrue(line.startswith('PROTOCOLINFO '))
+
+ def test_general_usage(self):
+ """
+ Checks a basic use case for the popping entries.
+ """
+
+ # pops a series of basic, space separated entries
+ line = stem.response.ControlLine(PROTOCOLINFO_RESPONSE[0])
+ self.assertEquals(line.remainder(), 'PROTOCOLINFO 1')
+ self.assertFalse(line.is_empty())
+ self.assertFalse(line.is_next_quoted())
+ self.assertFalse(line.is_next_mapping())
+ self.assertEquals(None, line.peek_key())
+
+ self.assertRaises(ValueError, line.pop_mapping)
+ self.assertEquals(line.pop(), 'PROTOCOLINFO')
+ self.assertEquals(line.remainder(), '1')
+ self.assertFalse(line.is_empty())
+ self.assertFalse(line.is_next_quoted())
+ self.assertFalse(line.is_next_mapping())
+ self.assertEquals(None, line.peek_key())
+
+ self.assertRaises(ValueError, line.pop_mapping)
+ self.assertEquals(line.pop(), '1')
+ self.assertEquals(line.remainder(), '')
+ self.assertTrue(line.is_empty())
+ self.assertFalse(line.is_next_quoted())
+ self.assertFalse(line.is_next_mapping())
+ self.assertEquals(None, line.peek_key())
+
+ self.assertRaises(IndexError, line.pop_mapping)
+ self.assertRaises(IndexError, line.pop)
+ self.assertEquals(line.remainder(), '')
+ self.assertTrue(line.is_empty())
+ self.assertFalse(line.is_next_quoted())
+ self.assertFalse(line.is_next_mapping())
+ self.assertEquals(None, line.peek_key())
+
+ def test_pop_mapping(self):
+ """
+ Checks use cases when parsing KEY=VALUE mappings.
+ """
+
+ # version entry with a space
+ version_entry = 'Tor="0.2.1.30 (0a083b0188cacd2f07838ff0446113bd5211a024)"'
+
+ line = stem.response.ControlLine(version_entry)
+ self.assertEquals(line.remainder(), version_entry)
+ self.assertFalse(line.is_empty())
+ self.assertFalse(line.is_next_quoted())
+ self.assertTrue(line.is_next_mapping())
+ self.assertTrue(line.is_next_mapping(key = "Tor"))
+ self.assertTrue(line.is_next_mapping(key = "Tor", quoted = True))
+ self.assertTrue(line.is_next_mapping(quoted = True))
+ self.assertEquals("Tor", line.peek_key())
+
+ # try popping this as a non-quoted mapping
+ self.assertEquals(line.pop_mapping(), ('Tor', '"0.2.1.30'))
+ self.assertEquals(line.remainder(), '(0a083b0188cacd2f07838ff0446113bd5211a024)"')
+ self.assertFalse(line.is_empty())
+ self.assertFalse(line.is_next_quoted())
+ self.assertFalse(line.is_next_mapping())
+ self.assertRaises(ValueError, line.pop_mapping)
+ self.assertEquals(None, line.peek_key())
+
+ # try popping this as a quoted mapping
+ line = stem.response.ControlLine(version_entry)
+ self.assertEquals(line.pop_mapping(True), ('Tor', '0.2.1.30 (0a083b0188cacd2f07838ff0446113bd5211a024)'))
+ self.assertEquals(line.remainder(), '')
+ self.assertTrue(line.is_empty())
+ self.assertFalse(line.is_next_quoted())
+ self.assertFalse(line.is_next_mapping())
+ self.assertEquals(None, line.peek_key())
+
+ def test_escapes(self):
+ """
+ Checks that we can parse quoted values with escaped quotes in it. This
+ explicitely comes up with the COOKIEFILE attribute of PROTOCOLINFO
+ responses.
+ """
+
+ auth_line = PROTOCOLINFO_RESPONSE[1]
+ line = stem.response.ControlLine(auth_line)
+ self.assertEquals(line, auth_line)
+ self.assertEquals(line.remainder(), auth_line)
+
+ self.assertEquals(line.pop(), "AUTH")
+ self.assertEquals(line.pop_mapping(), ("METHODS", "COOKIE"))
+
+ self.assertEquals(line.remainder(), r'COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie"')
+ self.assertTrue(line.is_next_mapping())
+ self.assertTrue(line.is_next_mapping(key = "COOKIEFILE"))
+ self.assertTrue(line.is_next_mapping(quoted = True))
+ self.assertTrue(line.is_next_mapping(quoted = True, escaped = True))
+ cookie_file_entry = line.remainder()
+
+ # try a general pop
+ self.assertEquals(line.pop(), 'COOKIEFILE="/tmp/my')
+ self.assertEquals(line.pop(), r'data\\\"dir//control_auth_cookie"')
+ self.assertTrue(line.is_empty())
+
+ # try a general pop with escapes
+ line = stem.response.ControlLine(cookie_file_entry)
+ self.assertEquals(line.pop(escaped = True), 'COOKIEFILE="/tmp/my')
+ self.assertEquals(line.pop(escaped = True), r'data\"dir//control_auth_cookie"')
+ self.assertTrue(line.is_empty())
+
+ # try a mapping pop
+ line = stem.response.ControlLine(cookie_file_entry)
+ self.assertEquals(line.pop_mapping(), ('COOKIEFILE', '"/tmp/my'))
+ self.assertEquals(line.remainder(), r'data\\\"dir//control_auth_cookie"')
+ self.assertFalse(line.is_empty())
+
+ # try a quoted mapping pop (this should trip up on the escaped quote)
+ line = stem.response.ControlLine(cookie_file_entry)
+ self.assertEquals(line.pop_mapping(True), ('COOKIEFILE', '/tmp/my data\\\\\\'))
+ self.assertEquals(line.remainder(), 'dir//control_auth_cookie"')
+ self.assertFalse(line.is_empty())
+
+ # try an escaped quoted mapping pop
+ line = stem.response.ControlLine(cookie_file_entry)
+ self.assertEquals(line.pop_mapping(True, True), ('COOKIEFILE', r'/tmp/my data\"dir//control_auth_cookie'))
+ self.assertTrue(line.is_empty())
+
diff --git a/test/unit/response/control_message.py b/test/unit/response/control_message.py
new file mode 100644
index 0000000..b86fc41
--- /dev/null
+++ b/test/unit/response/control_message.py
@@ -0,0 +1,184 @@
+"""
+Unit tests for the stem.response.ControlMessage parsing and class.
+"""
+
+import socket
+import StringIO
+import unittest
+import stem.socket
+
+OK_REPLY = "250 OK\r\n"
+
+EVENT_BW = "650 BW 32326 2856\r\n"
+EVENT_CIRC_TIMEOUT = "650 CIRC 5 FAILED PURPOSE=GENERAL REASON=TIMEOUT\r\n"
+EVENT_CIRC_LAUNCHED = "650 CIRC 9 LAUNCHED PURPOSE=GENERAL\r\n"
+EVENT_CIRC_EXTENDED = "650 CIRC 5 EXTENDED $A200F527C82C59A25CCA44884B49D3D65B122652=faktor PURPOSE=MEASURE_TIMEOUT\r\n"
+
+GETINFO_VERSION = """250-version=0.2.2.23-alpha (git-b85eb949b528f4d7)
+250 OK
+""".replace("\n", "\r\n")
+
+GETINFO_INFONAMES = """250+info/names=
+accounting/bytes -- Number of bytes read/written so far in the accounting interval.
+accounting/bytes-left -- Number of bytes left to write/read so far in the accounting interval.
+accounting/enabled -- Is accounting currently enabled?
+accounting/hibernating -- Are we hibernating or awake?
+stream-status -- List of current streams.
+version -- The current version of Tor.
+.
+250 OK
+""".replace("\n", "\r\n")
+
+class TestControlMessage(unittest.TestCase):
+ def test_ok_response(self):
+ """
+ Checks the basic 'OK' response that we get for most commands.
+ """
+
+ message = self._assert_message_parses(OK_REPLY)
+ self.assertEquals("OK", str(message))
+
+ contents = message.content()
+ self.assertEquals(1, len(contents))
+ self.assertEquals(("250", " ", "OK"), contents[0])
+
+ def test_event_response(self):
+ """
+ Checks parsing of actual events.
+ """
+
+ # BW event
+ message = self._assert_message_parses(EVENT_BW)
+ self.assertEquals("BW 32326 2856", str(message))
+
+ contents = message.content()
+ self.assertEquals(1, len(contents))
+ self.assertEquals(("650", " ", "BW 32326 2856"), contents[0])
+
+ # few types of CIRC events
+ for circ_content in (EVENT_CIRC_TIMEOUT, EVENT_CIRC_LAUNCHED, EVENT_CIRC_EXTENDED):
+ message = self._assert_message_parses(circ_content)
+ self.assertEquals(circ_content[4:-2], str(message))
+
+ contents = message.content()
+ self.assertEquals(1, len(contents))
+ self.assertEquals(("650", " ", str(message)), contents[0])
+
+ def test_getinfo_response(self):
+ """
+ Checks parsing of actual GETINFO responses.
+ """
+
+ # GETINFO version (basic single-line results)
+ message = self._assert_message_parses(GETINFO_VERSION)
+ self.assertEquals(2, len(list(message)))
+ self.assertEquals(2, len(str(message).splitlines()))
+
+ # manually checks the contents
+ contents = message.content()
+ self.assertEquals(2, len(contents))
+ self.assertEquals(("250", "-", "version=0.2.2.23-alpha (git-b85eb949b528f4d7)"), contents[0])
+ self.assertEquals(("250", " ", "OK"), contents[1])
+
+ # GETINFO info/names (data entry)
+ message = self._assert_message_parses(GETINFO_INFONAMES)
+ self.assertEquals(2, len(list(message)))
+ self.assertEquals(8, len(str(message).splitlines()))
+
+ # manually checks the contents
+ contents = message.content()
+ self.assertEquals(2, len(contents))
+
+ first_entry = (contents[0][0], contents[0][1], contents[0][2][:contents[0][2].find("\n")])
+ self.assertEquals(("250", "+", "info/names="), first_entry)
+ self.assertEquals(("250", " ", "OK"), contents[1])
+
+ def test_no_crlf(self):
+ """
+ Checks that we get a ProtocolError when we don't have both a carrage
+ returna and newline for line endings. This doesn't really check for
+ newlines (since that's what readline would break on), but not the end of
+ the world.
+ """
+
+ # Replaces each of the CRLF entries with just LF, confirming that this
+ # causes a parsing error. This should test line endings for both data
+ # entry parsing and non-data.
+
+ infonames_lines = [line + "\n" for line in GETINFO_INFONAMES.splitlines()]
+
+ for i in range(len(infonames_lines)):
+ # replace the CRLF for the line
+ infonames_lines[i] = infonames_lines[i].rstrip("\r\n") + "\n"
+ test_socket_file = StringIO.StringIO("".join(infonames_lines))
+ self.assertRaises(stem.socket.ProtocolError, stem.socket.recv_message, test_socket_file)
+
+ # puts the CRLF back
+ infonames_lines[i] = infonames_lines[i].rstrip("\n") + "\r\n"
+
+ # sanity check the above test isn't broken due to leaving infonames_lines
+ # with invalid data
+
+ self._assert_message_parses("".join(infonames_lines))
+
+ def test_malformed_prefix(self):
+ """
+ Checks parsing for responses where the header is missing a digit or divider.
+ """
+
+ for i in range(len(EVENT_BW)):
+ # makes test input with that character missing or replaced
+ removal_test_input = EVENT_BW[:i] + EVENT_BW[i + 1:]
+ replacement_test_input = EVENT_BW[:i] + "#" + EVENT_BW[i + 1:]
+
+ if i < 4 or i >= (len(EVENT_BW) - 2):
+ # dropping the character should cause an error if...
+ # - this is part of the message prefix
+ # - this is disrupting the line ending
+
+ self.assertRaises(stem.socket.ProtocolError, stem.socket.recv_message, StringIO.StringIO(removal_test_input))
+ self.assertRaises(stem.socket.ProtocolError, stem.socket.recv_message, StringIO.StringIO(replacement_test_input))
+ else:
+ # otherwise the data will be malformed, but this goes undetected
+ self._assert_message_parses(removal_test_input)
+ self._assert_message_parses(replacement_test_input)
+
+ def test_disconnected_socket(self):
+ """
+ Tests when the read function is given a file derived from a disconnected
+ socket.
+ """
+
+ control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ control_socket_file = control_socket.makefile()
+ self.assertRaises(stem.socket.SocketClosed, stem.socket.recv_message, control_socket_file)
+
+ def _assert_message_parses(self, controller_reply):
+ """
+ Performs some basic sanity checks that a reply mirrors its parsed result.
+
+ Returns:
+ stem.response.ControlMessage for the given input
+ """
+
+ message = stem.socket.recv_message(StringIO.StringIO(controller_reply))
+
+ # checks that the raw_content equals the input value
+ self.assertEqual(controller_reply, message.raw_content())
+
+ # checks that the contents match the input
+ message_lines = str(message).splitlines()
+ controller_lines = controller_reply.split("\r\n")
+ controller_lines.pop() # the ControlMessage won't have a trailing newline
+
+ while controller_lines:
+ line = controller_lines.pop(0)
+
+ # mismatching lines with just a period are probably data termination
+ if line == "." and (not message_lines or line != message_lines[0]):
+ continue
+
+ self.assertTrue(line.endswith(message_lines.pop(0)))
+
+ return message
+
diff --git a/test/unit/response/getinfo.py b/test/unit/response/getinfo.py
index 6787cff..5e82404 100644
--- a/test/unit/response/getinfo.py
+++ b/test/unit/response/getinfo.py
@@ -4,6 +4,7 @@ Unit tests for the stem.response.getinfo.GetInfoResponse class.
import unittest
+import stem.socket
import stem.response
import stem.response.getinfo
import test.mocking as mocking
@@ -53,7 +54,7 @@ class TestGetInfoResponse(unittest.TestCase):
stem.response.convert("GETINFO", control_message)
# now this should be a GetInfoResponse (ControlMessage subclass)
- self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
+ self.assertTrue(isinstance(control_message, stem.response.ControlMessage))
self.assertTrue(isinstance(control_message, stem.response.getinfo.GetInfoResponse))
self.assertEqual({}, control_message.entries)
diff --git a/test/unit/response/protocolinfo.py b/test/unit/response/protocolinfo.py
index 1d282ea..c82c8f8 100644
--- a/test/unit/response/protocolinfo.py
+++ b/test/unit/response/protocolinfo.py
@@ -58,7 +58,7 @@ class TestProtocolInfoResponse(unittest.TestCase):
stem.response.convert("PROTOCOLINFO", control_message)
# now this should be a ProtocolInfoResponse (ControlMessage subclass)
- self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
+ self.assertTrue(isinstance(control_message, stem.response.ControlMessage))
self.assertTrue(isinstance(control_message, stem.response.protocolinfo.ProtocolInfoResponse))
# exercise some of the ControlMessage functionality
diff --git a/test/unit/socket/__init__.py b/test/unit/socket/__init__.py
deleted file mode 100644
index a65ef67..0000000
--- a/test/unit/socket/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""
-Unit tests for stem.socket.
-"""
-
-__all__ = ["control_message", "control_line"]
-
diff --git a/test/unit/socket/control_line.py b/test/unit/socket/control_line.py
deleted file mode 100644
index 7752147..0000000
--- a/test/unit/socket/control_line.py
+++ /dev/null
@@ -1,166 +0,0 @@
-"""
-Unit tests for the stem.socket.ControlLine class.
-"""
-
-import unittest
-import stem.socket
-
-# response made by having 'DataDirectory /tmp/my data\"dir/' in the torrc
-PROTOCOLINFO_RESPONSE = (
- 'PROTOCOLINFO 1',
- 'AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\\\\\\"dir//control_auth_cookie"',
- 'VERSION Tor="0.2.1.30"',
- 'OK',
-)
-
-class TestControlLine(unittest.TestCase):
- def test_pop_examples(self):
- """
- Checks that the pop method's pydoc examples are correct.
- """
-
- line = stem.socket.ControlLine("\"We're all mad here.\" says the grinning cat.")
- self.assertEquals(line.pop(True), "We're all mad here.")
- self.assertEquals(line.pop(), "says")
- self.assertEquals(line.remainder(), "the grinning cat.")
-
- line = stem.socket.ControlLine("\"this has a \\\" and \\\\ in it\" foo=bar more_data")
- self.assertEquals(line.pop(True, True), "this has a \" and \\ in it")
-
- def test_string(self):
- """
- Basic checks that we behave as a regular immutable string.
- """
-
- line = stem.socket.ControlLine(PROTOCOLINFO_RESPONSE[0])
- self.assertEquals(line, 'PROTOCOLINFO 1')
- self.assertTrue(line.startswith('PROTOCOLINFO '))
-
- # checks that popping items doesn't effect us
- line.pop()
- self.assertEquals(line, 'PROTOCOLINFO 1')
- self.assertTrue(line.startswith('PROTOCOLINFO '))
-
- def test_general_usage(self):
- """
- Checks a basic use case for the popping entries.
- """
-
- # pops a series of basic, space separated entries
- line = stem.socket.ControlLine(PROTOCOLINFO_RESPONSE[0])
- self.assertEquals(line.remainder(), 'PROTOCOLINFO 1')
- self.assertFalse(line.is_empty())
- self.assertFalse(line.is_next_quoted())
- self.assertFalse(line.is_next_mapping())
- self.assertEquals(None, line.peek_key())
-
- self.assertRaises(ValueError, line.pop_mapping)
- self.assertEquals(line.pop(), 'PROTOCOLINFO')
- self.assertEquals(line.remainder(), '1')
- self.assertFalse(line.is_empty())
- self.assertFalse(line.is_next_quoted())
- self.assertFalse(line.is_next_mapping())
- self.assertEquals(None, line.peek_key())
-
- self.assertRaises(ValueError, line.pop_mapping)
- self.assertEquals(line.pop(), '1')
- self.assertEquals(line.remainder(), '')
- self.assertTrue(line.is_empty())
- self.assertFalse(line.is_next_quoted())
- self.assertFalse(line.is_next_mapping())
- self.assertEquals(None, line.peek_key())
-
- self.assertRaises(IndexError, line.pop_mapping)
- self.assertRaises(IndexError, line.pop)
- self.assertEquals(line.remainder(), '')
- self.assertTrue(line.is_empty())
- self.assertFalse(line.is_next_quoted())
- self.assertFalse(line.is_next_mapping())
- self.assertEquals(None, line.peek_key())
-
- def test_pop_mapping(self):
- """
- Checks use cases when parsing KEY=VALUE mappings.
- """
-
- # version entry with a space
- version_entry = 'Tor="0.2.1.30 (0a083b0188cacd2f07838ff0446113bd5211a024)"'
-
- line = stem.socket.ControlLine(version_entry)
- self.assertEquals(line.remainder(), version_entry)
- self.assertFalse(line.is_empty())
- self.assertFalse(line.is_next_quoted())
- self.assertTrue(line.is_next_mapping())
- self.assertTrue(line.is_next_mapping(key = "Tor"))
- self.assertTrue(line.is_next_mapping(key = "Tor", quoted = True))
- self.assertTrue(line.is_next_mapping(quoted = True))
- self.assertEquals("Tor", line.peek_key())
-
- # try popping this as a non-quoted mapping
- self.assertEquals(line.pop_mapping(), ('Tor', '"0.2.1.30'))
- self.assertEquals(line.remainder(), '(0a083b0188cacd2f07838ff0446113bd5211a024)"')
- self.assertFalse(line.is_empty())
- self.assertFalse(line.is_next_quoted())
- self.assertFalse(line.is_next_mapping())
- self.assertRaises(ValueError, line.pop_mapping)
- self.assertEquals(None, line.peek_key())
-
- # try popping this as a quoted mapping
- line = stem.socket.ControlLine(version_entry)
- self.assertEquals(line.pop_mapping(True), ('Tor', '0.2.1.30 (0a083b0188cacd2f07838ff0446113bd5211a024)'))
- self.assertEquals(line.remainder(), '')
- self.assertTrue(line.is_empty())
- self.assertFalse(line.is_next_quoted())
- self.assertFalse(line.is_next_mapping())
- self.assertEquals(None, line.peek_key())
-
- def test_escapes(self):
- """
- Checks that we can parse quoted values with escaped quotes in it. This
- explicitely comes up with the COOKIEFILE attribute of PROTOCOLINFO
- responses.
- """
-
- auth_line = PROTOCOLINFO_RESPONSE[1]
- line = stem.socket.ControlLine(auth_line)
- self.assertEquals(line, auth_line)
- self.assertEquals(line.remainder(), auth_line)
-
- self.assertEquals(line.pop(), "AUTH")
- self.assertEquals(line.pop_mapping(), ("METHODS", "COOKIE"))
-
- self.assertEquals(line.remainder(), r'COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie"')
- self.assertTrue(line.is_next_mapping())
- self.assertTrue(line.is_next_mapping(key = "COOKIEFILE"))
- self.assertTrue(line.is_next_mapping(quoted = True))
- self.assertTrue(line.is_next_mapping(quoted = True, escaped = True))
- cookie_file_entry = line.remainder()
-
- # try a general pop
- self.assertEquals(line.pop(), 'COOKIEFILE="/tmp/my')
- self.assertEquals(line.pop(), r'data\\\"dir//control_auth_cookie"')
- self.assertTrue(line.is_empty())
-
- # try a general pop with escapes
- line = stem.socket.ControlLine(cookie_file_entry)
- self.assertEquals(line.pop(escaped = True), 'COOKIEFILE="/tmp/my')
- self.assertEquals(line.pop(escaped = True), r'data\"dir//control_auth_cookie"')
- self.assertTrue(line.is_empty())
-
- # try a mapping pop
- line = stem.socket.ControlLine(cookie_file_entry)
- self.assertEquals(line.pop_mapping(), ('COOKIEFILE', '"/tmp/my'))
- self.assertEquals(line.remainder(), r'data\\\"dir//control_auth_cookie"')
- self.assertFalse(line.is_empty())
-
- # try a quoted mapping pop (this should trip up on the escaped quote)
- line = stem.socket.ControlLine(cookie_file_entry)
- self.assertEquals(line.pop_mapping(True), ('COOKIEFILE', '/tmp/my data\\\\\\'))
- self.assertEquals(line.remainder(), 'dir//control_auth_cookie"')
- self.assertFalse(line.is_empty())
-
- # try an escaped quoted mapping pop
- line = stem.socket.ControlLine(cookie_file_entry)
- self.assertEquals(line.pop_mapping(True, True), ('COOKIEFILE', r'/tmp/my data\"dir//control_auth_cookie'))
- self.assertTrue(line.is_empty())
-
diff --git a/test/unit/socket/control_message.py b/test/unit/socket/control_message.py
deleted file mode 100644
index 6d246e4..0000000
--- a/test/unit/socket/control_message.py
+++ /dev/null
@@ -1,184 +0,0 @@
-"""
-Unit tests for the stem.socket.ControlMessage parsing and class.
-"""
-
-import socket
-import StringIO
-import unittest
-import stem.socket
-
-OK_REPLY = "250 OK\r\n"
-
-EVENT_BW = "650 BW 32326 2856\r\n"
-EVENT_CIRC_TIMEOUT = "650 CIRC 5 FAILED PURPOSE=GENERAL REASON=TIMEOUT\r\n"
-EVENT_CIRC_LAUNCHED = "650 CIRC 9 LAUNCHED PURPOSE=GENERAL\r\n"
-EVENT_CIRC_EXTENDED = "650 CIRC 5 EXTENDED $A200F527C82C59A25CCA44884B49D3D65B122652=faktor PURPOSE=MEASURE_TIMEOUT\r\n"
-
-GETINFO_VERSION = """250-version=0.2.2.23-alpha (git-b85eb949b528f4d7)
-250 OK
-""".replace("\n", "\r\n")
-
-GETINFO_INFONAMES = """250+info/names=
-accounting/bytes -- Number of bytes read/written so far in the accounting interval.
-accounting/bytes-left -- Number of bytes left to write/read so far in the accounting interval.
-accounting/enabled -- Is accounting currently enabled?
-accounting/hibernating -- Are we hibernating or awake?
-stream-status -- List of current streams.
-version -- The current version of Tor.
-.
-250 OK
-""".replace("\n", "\r\n")
-
-class TestControlMessage(unittest.TestCase):
- def test_ok_response(self):
- """
- Checks the basic 'OK' response that we get for most commands.
- """
-
- message = self._assert_message_parses(OK_REPLY)
- self.assertEquals("OK", str(message))
-
- contents = message.content()
- self.assertEquals(1, len(contents))
- self.assertEquals(("250", " ", "OK"), contents[0])
-
- def test_event_response(self):
- """
- Checks parsing of actual events.
- """
-
- # BW event
- message = self._assert_message_parses(EVENT_BW)
- self.assertEquals("BW 32326 2856", str(message))
-
- contents = message.content()
- self.assertEquals(1, len(contents))
- self.assertEquals(("650", " ", "BW 32326 2856"), contents[0])
-
- # few types of CIRC events
- for circ_content in (EVENT_CIRC_TIMEOUT, EVENT_CIRC_LAUNCHED, EVENT_CIRC_EXTENDED):
- message = self._assert_message_parses(circ_content)
- self.assertEquals(circ_content[4:-2], str(message))
-
- contents = message.content()
- self.assertEquals(1, len(contents))
- self.assertEquals(("650", " ", str(message)), contents[0])
-
- def test_getinfo_response(self):
- """
- Checks parsing of actual GETINFO responses.
- """
-
- # GETINFO version (basic single-line results)
- message = self._assert_message_parses(GETINFO_VERSION)
- self.assertEquals(2, len(list(message)))
- self.assertEquals(2, len(str(message).splitlines()))
-
- # manually checks the contents
- contents = message.content()
- self.assertEquals(2, len(contents))
- self.assertEquals(("250", "-", "version=0.2.2.23-alpha (git-b85eb949b528f4d7)"), contents[0])
- self.assertEquals(("250", " ", "OK"), contents[1])
-
- # GETINFO info/names (data entry)
- message = self._assert_message_parses(GETINFO_INFONAMES)
- self.assertEquals(2, len(list(message)))
- self.assertEquals(8, len(str(message).splitlines()))
-
- # manually checks the contents
- contents = message.content()
- self.assertEquals(2, len(contents))
-
- first_entry = (contents[0][0], contents[0][1], contents[0][2][:contents[0][2].find("\n")])
- self.assertEquals(("250", "+", "info/names="), first_entry)
- self.assertEquals(("250", " ", "OK"), contents[1])
-
- def test_no_crlf(self):
- """
- Checks that we get a ProtocolError when we don't have both a carrage
- returna and newline for line endings. This doesn't really check for
- newlines (since that's what readline would break on), but not the end of
- the world.
- """
-
- # Replaces each of the CRLF entries with just LF, confirming that this
- # causes a parsing error. This should test line endings for both data
- # entry parsing and non-data.
-
- infonames_lines = [line + "\n" for line in GETINFO_INFONAMES.splitlines()]
-
- for i in range(len(infonames_lines)):
- # replace the CRLF for the line
- infonames_lines[i] = infonames_lines[i].rstrip("\r\n") + "\n"
- test_socket_file = StringIO.StringIO("".join(infonames_lines))
- self.assertRaises(stem.socket.ProtocolError, stem.socket.recv_message, test_socket_file)
-
- # puts the CRLF back
- infonames_lines[i] = infonames_lines[i].rstrip("\n") + "\r\n"
-
- # sanity check the above test isn't broken due to leaving infonames_lines
- # with invalid data
-
- self._assert_message_parses("".join(infonames_lines))
-
- def test_malformed_prefix(self):
- """
- Checks parsing for responses where the header is missing a digit or divider.
- """
-
- for i in range(len(EVENT_BW)):
- # makes test input with that character missing or replaced
- removal_test_input = EVENT_BW[:i] + EVENT_BW[i + 1:]
- replacement_test_input = EVENT_BW[:i] + "#" + EVENT_BW[i + 1:]
-
- if i < 4 or i >= (len(EVENT_BW) - 2):
- # dropping the character should cause an error if...
- # - this is part of the message prefix
- # - this is disrupting the line ending
-
- self.assertRaises(stem.socket.ProtocolError, stem.socket.recv_message, StringIO.StringIO(removal_test_input))
- self.assertRaises(stem.socket.ProtocolError, stem.socket.recv_message, StringIO.StringIO(replacement_test_input))
- else:
- # otherwise the data will be malformed, but this goes undetected
- self._assert_message_parses(removal_test_input)
- self._assert_message_parses(replacement_test_input)
-
- def test_disconnected_socket(self):
- """
- Tests when the read function is given a file derived from a disconnected
- socket.
- """
-
- control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- control_socket_file = control_socket.makefile()
- self.assertRaises(stem.socket.SocketClosed, stem.socket.recv_message, control_socket_file)
-
- def _assert_message_parses(self, controller_reply):
- """
- Performs some basic sanity checks that a reply mirrors its parsed result.
-
- Returns:
- stem.socket.ControlMessage for the given input
- """
-
- message = stem.socket.recv_message(StringIO.StringIO(controller_reply))
-
- # checks that the raw_content equals the input value
- self.assertEqual(controller_reply, message.raw_content())
-
- # checks that the contents match the input
- message_lines = str(message).splitlines()
- controller_lines = controller_reply.split("\r\n")
- controller_lines.pop() # the ControlMessage won't have a trailing newline
-
- while controller_lines:
- line = controller_lines.pop(0)
-
- # mismatching lines with just a period are probably data termination
- if line == "." and (not message_lines or line != message_lines[0]):
- continue
-
- self.assertTrue(line.endswith(message_lines.pop(0)))
-
- return message
-
1
0
commit 66a1ef5ebc348fc2276541c8cef58067d1d88a6d
Author: Translation commit bot <translation(a)torproject.org>
Date: Tue May 29 00:45:07 2012 +0000
Update translations for tsum
---
ru/short-user-manual_ru_noimg.xhtml | 8 ++++----
1 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/ru/short-user-manual_ru_noimg.xhtml b/ru/short-user-manual_ru_noimg.xhtml
index 0523ee3..5cba14a 100644
--- a/ru/short-user-manual_ru_noimg.xhtml
+++ b/ru/short-user-manual_ru_noimg.xhtml
@@ -5,14 +5,14 @@
</head>
<body>
<h1 id="the-short-user-manual">Краткое Руководство Пользователя</h1>
- <p>Это пользовательское руководство содержит информацию о том, как загрузить Tor, как им пользоваться, и что предпринимать в случаях, когда Tor не может соединиться с сетью. Если у Вас не получилось найти в этом документе ответ на свой вопрос, не стесняйтесь написать нам по адресу help(a)rt.torproject.org.</p>
+ <p>Это пользовательское руководство расскажет о том, как загрузить Tor, как им пользоваться, и что предпринимать в случаях, когда Tor не может соединиться с сетью. Если у Вас не получилось найти в этом документе ответ на свой вопрос, не стесняйтесь написать нам по адресу help(a)rt.torproject.org.</p>
<p>Пожалуйста, помните, что мы осуществляем техподдержку на добровольных началах, причём каждый Божий день нам приходит куча писем с вопросами. Поэтому не волнуйтесь, если мы медлим с ответом на Ваше письмо.</p>
<h2 id="how-tor-works">Как работает Tor</h2>
<p>Tor - это сеть виртуальных туннелей, которая позволяет Вам лучше защищать неприкосновенность Вашей частной жизни и свою безопасность в Интернете. Работает это вот как: Tor соединяет Ваш компьютер с Интернетом не напрямую, а через цепочку из трёх выбранных наугад компьютеров (т.н. <em>ретрансляторы</em>) которые принадлежат к сети Tor.</p>
<p>На рисунке выше изображено, что происходит, когда пользователь посещает различные веб-сайты через Tor. Зелёные мониторы - это ретрансляторы сети Tor, а три ключика - это последовательные слои шифрования между пользователем и каждым следующим ретранслятором.</p>
- <p>Tor will anonymize the origin of your traffic, and it will encrypt everything between you and the Tor network. Tor will also encrypt your traffic inside the Tor network, but it cannot encrypt your traffic between the Tor network and its final destination.</p>
- <p>If you are communicating sensitive information, for example when logging on to a website with a username and password, make sure that you are using HTTPS (e.g. <strong>https</strong>://torproject.org/, not <strong>http</strong>://torproject.org/).</p>
- <h2 id="how-to-download-tor">How to download Tor</h2>
+ <p>Всё, что вы посылаете в Интернет через Tor, становится, во-первых анонимным (скрывается источник), а во-вторых, остаётся зашифрованным на всём пути между Вашим компьютером и последним ретранслятором. Но после того, как данные покидают последний ретранслятор и отправляются к интернет-адресу своего назначения - они идут уже не в зашифрованном, а обычном, открытом виде.</p>
+ <p>Если вы передаёте особо важные данные - например, вводите логин и пароль для входа на веб-сайт - убедитесь, что работает протокол HTTPS (т.е. в адресной строке написано, например, <strong>https</strong>://torproject.org/, а не <strong>http</strong>://torproject.org/).</p>
+ <h2 id="how-to-download-tor">Как загрузить Tor</h2>
<p>The bundle we recommend to most users is the <a href="https://www.torproject.org/projects/torbrowser.html">Tor Browser Bundle</a>. This bundle contains a browser preconfigured to safely browse the Internet through Tor, and requires no installation. You download the bundle, extract the archive, and start Tor.</p>
<p>There are two different ways to get hold of the Tor software. You can either browse to the <a href="https://www.torproject.org/">Tor Project website</a> and download it there, or you can use GetTor, the email autoresponder.</p>
<h3 id="how-to-get-tor-via-email">How to get Tor via email</h3>
1
0
commit 794662a6563e99e9f55b32603cab312842cd264a
Author: Tomás Touceda <chiiph(a)torproject.org>
Date: Mon May 28 20:53:40 2012 -0300
Disable run at start for TBB users
---
changes/disableRunAtStart | 3 +++
src/vidalia/config/GeneralPage.cpp | 5 +++++
2 files changed, 8 insertions(+), 0 deletions(-)
diff --git a/changes/disableRunAtStart b/changes/disableRunAtStart
new file mode 100644
index 0000000..f8890fc
--- /dev/null
+++ b/changes/disableRunAtStart
@@ -0,0 +1,3 @@
+ o Disable "Run Vidalia when my system starts" if the
+ BrowserExecutable config option is set. This will avoid issues with
+ TBB users starting Vidalia the wrong way.
\ No newline at end of file
diff --git a/src/vidalia/config/GeneralPage.cpp b/src/vidalia/config/GeneralPage.cpp
index b6db202..6b0a1ba 100644
--- a/src/vidalia/config/GeneralPage.cpp
+++ b/src/vidalia/config/GeneralPage.cpp
@@ -47,6 +47,11 @@ GeneralPage::GeneralPage(QWidget *parent)
#if !defined(USE_AUTOUPDATE)
ui.grpSoftwareUpdates->setVisible(false);
#endif
+
+ if (_vidaliaSettings->getBrowserExecutable().length() > 0) {
+ ui.lineHorizontalSeparator->setVisible(false);
+ ui.chkRunVidaliaAtSystemStartup->setVisible(false);
+ }
}
/** Destructor */
1
0