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
November 2011
- 16 participants
- 987 discussions

27 Nov '11
commit 841247a5864064ce031988ca2dab5808b11408b2
Author: Sebastian Hahn <sebastian(a)torproject.org>
Date: Wed Nov 23 01:41:37 2011 +0100
Fix a compile warning on 64bit OS X
Backport of 68475fc5c5a806ebbb5657de1667dab2c3e09b7c which accidentally
only made it into master. Fixes bug 4547. Bug isn't in any released
version.
---
src/or/dirserv.c | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
diff --git a/src/or/dirserv.c b/src/or/dirserv.c
index 19d9702..634b3ec 100644
--- a/src/or/dirserv.c
+++ b/src/or/dirserv.c
@@ -1909,7 +1909,7 @@ dirserv_compute_performance_thresholds(routerlist_t *rl)
* of Fast nodes. */
if (options->AuthDirFastGuarantee &&
fast_bandwidth > options->AuthDirFastGuarantee)
- fast_bandwidth = options->AuthDirFastGuarantee;
+ fast_bandwidth = (uint32_t)options->AuthDirFastGuarantee;
/* Now that we have a time-known that 7/8 routers are known longer than,
* fill wfus with the wfu of every such "familiar" router. */
1
0

[tor/release-0.2.2] Correct the handling of overflow behavior in smartlist_ensure_capacity
by arma@torproject.org 27 Nov '11
by arma@torproject.org 27 Nov '11
27 Nov '11
commit e1c6431e42bc6b5619c78008cbe92730ead57bbf
Author: Nick Mathewson <nickm(a)torproject.org>
Date: Wed Nov 9 12:08:28 2011 -0500
Correct the handling of overflow behavior in smartlist_ensure_capacity
The old behavior was susceptible to the compiler optimizing out our
assertion check, *and* could still overflow size_t on 32-bit systems
even when it did work.
---
changes/bug4230 | 5 +++++
src/common/container.c | 19 ++++++++++++++-----
2 files changed, 19 insertions(+), 5 deletions(-)
diff --git a/changes/bug4230 b/changes/bug4230
new file mode 100644
index 0000000..c1ba584
--- /dev/null
+++ b/changes/bug4230
@@ -0,0 +1,5 @@
+ o Minor bugfixes:
+ - Resolve an integer overflow bug in smartlist_ensure_capacity.
+ Fixes bug 4230; bugfix on Tor 0.1.0.1-rc. Based on a patch by
+ Mansour Moufid.
+
diff --git a/src/common/container.c b/src/common/container.c
index c741eb0..edfcd97 100644
--- a/src/common/container.c
+++ b/src/common/container.c
@@ -61,13 +61,22 @@ smartlist_clear(smartlist_t *sl)
static INLINE void
smartlist_ensure_capacity(smartlist_t *sl, int size)
{
+#if SIZEOF_SIZE_T > SIZEOF_INT
+#define MAX_CAPACITY (INT_MAX)
+#else
+#define MAX_CAPACITY (int)((SIZE_MAX / (sizeof(void*))))
+#endif
if (size > sl->capacity) {
- int higher = sl->capacity * 2;
- while (size > higher)
- higher *= 2;
- tor_assert(higher > 0); /* detect overflow */
+ int higher = sl->capacity;
+ if (PREDICT_UNLIKELY(size > MAX_CAPACITY/2)) {
+ tor_assert(size <= MAX_CAPACITY);
+ higher = MAX_CAPACITY;
+ } else {
+ while (size > higher)
+ higher *= 2;
+ }
sl->capacity = higher;
- sl->list = tor_realloc(sl->list, sizeof(void*)*sl->capacity);
+ sl->list = tor_realloc(sl->list, sizeof(void*)*((size_t)sl->capacity));
}
}
1
0

[tor/release-0.2.2] Merge remote-tracking branch 'public/bug4230' into maint-0.2.2
by arma@torproject.org 27 Nov '11
by arma@torproject.org 27 Nov '11
27 Nov '11
commit fbf1c5ee79490577ec0b8c68338ba4f872e993b4
Merge: 841247a e1c6431
Author: Nick Mathewson <nickm(a)torproject.org>
Date: Wed Nov 23 16:22:26 2011 -0500
Merge remote-tracking branch 'public/bug4230' into maint-0.2.2
changes/bug4230 | 5 +++++
src/common/container.c | 19 ++++++++++++++-----
2 files changed, 19 insertions(+), 5 deletions(-)
1
0

27 Nov '11
commit 57db578b814ec0c036048135943da9942b72953d
Merge: 842a7a8 fbf1c5e
Author: Roger Dingledine <arma(a)torproject.org>
Date: Sun Nov 27 03:33:11 2011 -0500
Merge branch 'maint-0.2.2' into release-0.2.2
changes/bug4230 | 5 +++++
src/common/container.c | 19 ++++++++++++++-----
src/or/dirserv.c | 2 +-
3 files changed, 20 insertions(+), 6 deletions(-)
1
0

26 Nov '11
commit 62ecde1d4432eb96a3346289c8257561baa4faff
Author: Damian Johnson <atagar(a)torproject.org>
Date: Fri Nov 25 22:13:56 2011 -0800
Refactoring version functions into module
Moving the last of the types.py contents and a related function from process.py
into a module specifically for handling tor versions and requirements (the
later part will grow as the library matures).
---
run_tests.py | 4 +-
stem/__init__.py | 2 +-
stem/connection.py | 16 ++--
stem/process.py | 48 -----------
stem/types.py | 98 ---------------------
stem/version.py | 156 ++++++++++++++++++++++++++++++++++
test/integ/socket/control_message.py | 7 +-
test/unit/connection/protocolinfo.py | 5 +-
test/unit/types/__init__.py | 6 --
test/unit/types/version.py | 133 -----------------------------
test/unit/version.py | 133 +++++++++++++++++++++++++++++
11 files changed, 307 insertions(+), 301 deletions(-)
diff --git a/run_tests.py b/run_tests.py
index ba74b66..8fa7107 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -13,9 +13,9 @@ import unittest
import StringIO
import test.runner
+import test.unit.version
import test.unit.socket.control_message
import test.unit.socket.control_line
-import test.unit.types.version
import test.unit.connection.protocolinfo
import test.unit.util.enum
import test.unit.util.system
@@ -34,7 +34,7 @@ DIVIDER = "=" * 70
# (name, class) tuples for all of our unit and integration tests
UNIT_TESTS = (("stem.socket.ControlMessage", test.unit.socket.control_message.TestControlMessage),
("stem.socket.ControlLine", test.unit.socket.control_line.TestControlLine),
- ("stem.types.Version", test.unit.types.version.TestVerion),
+ ("stem.types.Version", test.unit.version.TestVerion),
("stem.connection.ProtocolInfoResponse", test.unit.connection.protocolinfo.TestProtocolInfoResponse),
("stem.util.enum", test.unit.util.enum.TestEnum),
("stem.util.system", test.unit.util.system.TestSystem),
diff --git a/stem/__init__.py b/stem/__init__.py
index d4ede49..5b2cdd0 100644
--- a/stem/__init__.py
+++ b/stem/__init__.py
@@ -4,5 +4,5 @@ Library for working with the tor process.
import stem.util # suppresses log handler warnings
-__all__ = ["connection", "process", "socket", "types"]
+__all__ = ["connection", "process", "socket", "version"]
diff --git a/stem/connection.py b/stem/connection.py
index ac548aa..ba851a2 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -21,7 +21,7 @@ import logging
import threading
import stem.socket
-import stem.types
+import stem.version
import stem.util.enum
import stem.util.system
@@ -176,12 +176,12 @@ class ProtocolInfoResponse(stem.socket.ControlMessage):
response, so all other values are None if undefined or empty if a collecion.
Attributes:
- protocol_version (int) - protocol version of the response
- tor_version (stem.types.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
- socket (socket.socket) - socket used to make the query
+ 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
+ socket (socket.socket) - socket used to make the query
"""
def convert(control_message):
@@ -302,7 +302,7 @@ class ProtocolInfoResponse(stem.socket.ControlMessage):
torversion = line.pop_mapping(True)[1]
try:
- self.tor_version = stem.types.Version(torversion)
+ self.tor_version = stem.version.Version(torversion)
except ValueError, exc:
raise stem.socket.ProtocolError(exc)
else:
diff --git a/stem/process.py b/stem/process.py
index 2974989..0bb9740 100644
--- a/stem/process.py
+++ b/stem/process.py
@@ -2,7 +2,6 @@
Helper functions for working with tor as a process. These are mostly os
dependent, only working on linux, osx, and bsd.
-get_tor_version - gets the version of our system's tor installation
launch_tor - starts up a tor process
"""
@@ -11,56 +10,9 @@ import os
import signal
import subprocess
-import stem.types
-import stem.util.system
-
# number of seconds before we time out our attempt to start a tor instance
DEFAULT_INIT_TIMEOUT = 90
-# cache for the get_tor_version function
-VERSION_CACHE = {}
-
-def get_tor_version(tor_cmd = "tor"):
- """
- Queries tor for its version.
-
- Arguments:
- tor_cmd (str) - command used to run tor
-
- Returns:
- stem.types.Version provided by the tor command
-
- Raises:
- IOError if unable to query or parse the version
- """
-
- if not tor_cmd in VERSION_CACHE:
- try:
- version_cmd = "%s --version" % tor_cmd
- version_output = stem.util.system.call(version_cmd)
- except OSError, exc:
- raise IOError(exc)
-
- if version_output:
- # output example:
- # Oct 21 07:19:27.438 [notice] Tor v0.2.1.30. This is experimental software. Do not rely on it for strong anonymity. (Running on Linux i686)
- # Tor version 0.2.1.30.
-
- last_line = version_output[-1]
-
- if last_line.startswith("Tor version ") and last_line.endswith("."):
- try:
- version_str = last_line[12:-1]
- VERSION_CACHE[tor_cmd] = stem.types.Version(version_str)
- except ValueError, exc:
- raise IOError(exc)
- else:
- raise IOError("Unexpected response from '%s': %s" % (version_cmd, last_line))
- else:
- raise IOError("'%s' didn't have any output" % version_cmd)
-
- return VERSION_CACHE[tor_cmd]
-
def launch_tor(torrc_path, completion_percent = 100, init_msg_handler = None, timeout = DEFAULT_INIT_TIMEOUT):
"""
Initializes a tor process. This blocks until initialization completes or we
diff --git a/stem/types.py b/stem/types.py
deleted file mode 100644
index 3d62582..0000000
--- a/stem/types.py
+++ /dev/null
@@ -1,98 +0,0 @@
-"""
-Class representations for a variety of tor objects. These are most commonly
-return values rather than being instantiated by users directly.
-
-Version - Tor versioning information.
- |- __str__ - string representation
- +- __cmp__ - compares with another Version
-"""
-
-import re
-import socket
-import logging
-import threading
-
-LOGGER = logging.getLogger("stem")
-
-class Version:
- """
- Comparable tor version, as per the 'new version' of the version-spec...
- https://gitweb.torproject.org/torspec.git/blob/HEAD:/version-spec.txt
-
- Attributes:
- major (int) - major version
- minor (int) - minor version
- micro (int) - micro version
- patch (int) - optional patch level (None if undefined)
- status (str) - optional status tag without the preceding dash such as
- 'alpha', 'beta-dev', etc (None if undefined)
- """
-
- def __init__(self, version_str):
- """
- Parses a valid tor version string, for instance "0.1.4" or
- "0.2.2.23-alpha".
-
- Raises:
- ValueError if input isn't a valid tor version
- """
-
- m = re.match(r'^([0-9]+).([0-9]+).([0-9]+)(.[0-9]+)?(-\S*)?$', version_str)
-
- if m:
- major, minor, micro, patch, status = m.groups()
-
- # The patch and status matches are optional (may be None) and have an extra
- # proceeding period or dash if they exist. Stripping those off.
-
- if patch: patch = int(patch[1:])
- if status: status = status[1:]
-
- self.major = int(major)
- self.minor = int(minor)
- self.micro = int(micro)
- self.patch = patch
- self.status = status
- else: raise ValueError("'%s' isn't a properly formatted tor version" % version_str)
-
- def __str__(self):
- """
- Provides the normal representation for the version, for instance:
- "0.2.2.23-alpha"
- """
-
- suffix = ""
-
- if self.patch:
- suffix += ".%i" % self.patch
-
- if self.status:
- suffix += "-%s" % self.status
-
- return "%i.%i.%i%s" % (self.major, self.minor, self.micro, suffix)
-
- def __cmp__(self, other):
- """
- Simple comparison of versions. An undefined patch level is treated as zero
- and status tags are compared lexically (as per the version spec).
- """
-
- if not isinstance(other, Version):
- return 1 # this is also used for equality checks
-
- for attr in ("major", "minor", "micro", "patch"):
- my_version = max(0, self.__dict__[attr])
- other_version = max(0, other.__dict__[attr])
-
- if my_version > other_version: return 1
- elif my_version < other_version: return -1
-
- my_status = self.status if self.status else ""
- other_status = other.status if other.status else ""
-
- return cmp(my_status, other_status)
-
-# TODO: version requirements will probably be moved to another module later
-REQ_GETINFO_CONFIG_TEXT = Version("0.2.2.7-alpha")
-REQ_CONTROL_SOCKET = Version("0.2.0.30")
-
diff --git a/stem/version.py b/stem/version.py
new file mode 100644
index 0000000..d417934
--- /dev/null
+++ b/stem/version.py
@@ -0,0 +1,156 @@
+"""
+Tor versioning information and requirements for its features. These can be
+easily parsed and compared, for instance...
+
+>>> my_version = stem.version.get_system_tor_version()
+>>> print my_version
+0.2.1.30
+>>> my_version > stem.version.Requirement.CONTROL_SOCKET
+True
+
+get_system_tor_version - gets the version of our system's tor installation
+Version - Tor versioning information.
+ |- __str__ - string representation
+ +- __cmp__ - compares with another Version
+
+Requirement - Enumerations for the version requirements of features.
+ |- GETINFO_CONFIG_TEXT - 'GETINFO config-text' query
+ +- CONTROL_SOCKET - 'ControlSocket <path>' config option
+"""
+
+import re
+import logging
+
+import stem.util.enum
+import stem.util.system
+
+LOGGER = logging.getLogger("stem")
+
+# cache for the get_tor_version function
+VERSION_CACHE = {}
+
+def get_system_tor_version(tor_cmd = "tor"):
+ """
+ Queries tor for its version. This is os dependent, only working on linux,
+ osx, and bsd.
+
+ Arguments:
+ tor_cmd (str) - command used to run tor
+
+ Returns:
+ stem.version.Version provided by the tor command
+
+ Raises:
+ IOError if unable to query or parse the version
+ """
+
+ if not tor_cmd in VERSION_CACHE:
+ try:
+ version_cmd = "%s --version" % tor_cmd
+ version_output = stem.util.system.call(version_cmd)
+ except OSError, exc:
+ raise IOError(exc)
+
+ if version_output:
+ # output example:
+ # Oct 21 07:19:27.438 [notice] Tor v0.2.1.30. This is experimental software. Do not rely on it for strong anonymity. (Running on Linux i686)
+ # Tor version 0.2.1.30.
+
+ last_line = version_output[-1]
+
+ if last_line.startswith("Tor version ") and last_line.endswith("."):
+ try:
+ version_str = last_line[12:-1]
+ VERSION_CACHE[tor_cmd] = Version(version_str)
+ except ValueError, exc:
+ raise IOError(exc)
+ else:
+ raise IOError("Unexpected response from '%s': %s" % (version_cmd, last_line))
+ else:
+ raise IOError("'%s' didn't have any output" % version_cmd)
+
+ return VERSION_CACHE[tor_cmd]
+
+class Version:
+ """
+ Comparable tor version, as per the 'new version' of the version-spec...
+ https://gitweb.torproject.org/torspec.git/blob/HEAD:/version-spec.txt
+
+ Attributes:
+ major (int) - major version
+ minor (int) - minor version
+ micro (int) - micro version
+ patch (int) - optional patch level (None if undefined)
+ status (str) - optional status tag without the preceding dash such as
+ 'alpha', 'beta-dev', etc (None if undefined)
+ """
+
+ def __init__(self, version_str):
+ """
+ Parses a valid tor version string, for instance "0.1.4" or
+ "0.2.2.23-alpha".
+
+ Raises:
+ ValueError if input isn't a valid tor version
+ """
+
+ m = re.match(r'^([0-9]+).([0-9]+).([0-9]+)(.[0-9]+)?(-\S*)?$', version_str)
+
+ if m:
+ major, minor, micro, patch, status = m.groups()
+
+ # The patch and status matches are optional (may be None) and have an extra
+ # proceeding period or dash if they exist. Stripping those off.
+
+ if patch: patch = int(patch[1:])
+ if status: status = status[1:]
+
+ self.major = int(major)
+ self.minor = int(minor)
+ self.micro = int(micro)
+ self.patch = patch
+ self.status = status
+ else: raise ValueError("'%s' isn't a properly formatted tor version" % version_str)
+
+ def __str__(self):
+ """
+ Provides the normal representation for the version, for instance:
+ "0.2.2.23-alpha"
+ """
+
+ suffix = ""
+
+ if self.patch:
+ suffix += ".%i" % self.patch
+
+ if self.status:
+ suffix += "-%s" % self.status
+
+ return "%i.%i.%i%s" % (self.major, self.minor, self.micro, suffix)
+
+ def __cmp__(self, other):
+ """
+ Simple comparison of versions. An undefined patch level is treated as zero
+ and status tags are compared lexically (as per the version spec).
+ """
+
+ if not isinstance(other, Version):
+ return 1 # this is also used for equality checks
+
+ for attr in ("major", "minor", "micro", "patch"):
+ my_version = max(0, self.__dict__[attr])
+ other_version = max(0, other.__dict__[attr])
+
+ if my_version > other_version: return 1
+ elif my_version < other_version: return -1
+
+ my_status = self.status if self.status else ""
+ other_status = other.status if other.status else ""
+
+ return cmp(my_status, other_status)
+
+Requirement = stem.util.enum.Enum(
+ ("GETINFO_CONFIG_TEXT", Version("0.2.2.7-alpha")),
+ ("CONTROL_SOCKET", Version("0.2.0.30")),
+)
+
diff --git a/test/integ/socket/control_message.py b/test/integ/socket/control_message.py
index bbcd222..2cdb96c 100644
--- a/test/integ/socket/control_message.py
+++ b/test/integ/socket/control_message.py
@@ -7,7 +7,7 @@ import socket
import unittest
import stem.socket
-import stem.types
+import stem.version
import test.runner
class TestControlMessage(unittest.TestCase):
@@ -139,8 +139,9 @@ class TestControlMessage(unittest.TestCase):
Parses the 'GETINFO config-text' response.
"""
- if stem.process.get_tor_version() < stem.types.REQ_GETINFO_CONFIG_TEXT:
- self.skipTest("(requires %s)" % stem.types.REQ_GETINFO_CONFIG_TEXT)
+ req_version = stem.version.Requirement.GETINFO_CONFIG_TEXT
+ if stem.version.get_system_tor_version() < req_version:
+ self.skipTest("(requires %s)" % req_version)
# We can't be certain of the order, and there may be extra config-text
# entries as per...
diff --git a/test/unit/connection/protocolinfo.py b/test/unit/connection/protocolinfo.py
index b597981..ecca8b6 100644
--- a/test/unit/connection/protocolinfo.py
+++ b/test/unit/connection/protocolinfo.py
@@ -4,9 +4,10 @@ Unit tests for the stem.connection.ProtocolInfoResponse class.
import unittest
import StringIO
+
import stem.connection
import stem.socket
-import stem.types
+import stem.version
NO_AUTH = """250-PROTOCOLINFO 1
250-AUTH METHODS=NULL
@@ -87,7 +88,7 @@ class TestProtocolInfoResponse(unittest.TestCase):
stem.connection.ProtocolInfoResponse.convert(control_message)
self.assertEquals(1, control_message.protocol_version)
- self.assertEquals(stem.types.Version("0.2.1.30"), control_message.tor_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)
diff --git a/test/unit/types/__init__.py b/test/unit/types/__init__.py
deleted file mode 100644
index 95da593..0000000
--- a/test/unit/types/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""
-Unit tests for stem.types.
-"""
-
-__all__ = ["version"]
-
diff --git a/test/unit/types/version.py b/test/unit/types/version.py
deleted file mode 100644
index 067759b..0000000
--- a/test/unit/types/version.py
+++ /dev/null
@@ -1,133 +0,0 @@
-"""
-Unit tests for the stem.types.Version parsing and class.
-"""
-
-import unittest
-import stem.types
-
-class TestVerion(unittest.TestCase):
- """
- Tests methods and functions related to 'stem.types.Version'.
- """
-
- def test_parsing(self):
- """
- Tests parsing by the Version class constructor.
- """
-
- # valid versions with various number of compontents to the version
- version = stem.types.Version("0.1.2.3-tag")
- self.assert_versions_match(version, 0, 1, 2, 3, "tag")
-
- version = stem.types.Version("0.1.2.3")
- self.assert_versions_match(version, 0, 1, 2, 3, None)
-
- version = stem.types.Version("0.1.2-tag")
- self.assert_versions_match(version, 0, 1, 2, None, "tag")
-
- version = stem.types.Version("0.1.2")
- self.assert_versions_match(version, 0, 1, 2, None, None)
-
- # checks an empty tag
- version = stem.types.Version("0.1.2.3-")
- self.assert_versions_match(version, 0, 1, 2, 3, "")
-
- version = stem.types.Version("0.1.2-")
- self.assert_versions_match(version, 0, 1, 2, None, "")
-
- # checks invalid version strings
- self.assertRaises(ValueError, stem.types.Version, "")
- self.assertRaises(ValueError, stem.types.Version, "1.2.3.4nodash")
- self.assertRaises(ValueError, stem.types.Version, "1.2.3.a")
- self.assertRaises(ValueError, stem.types.Version, "1.2.a.4")
- self.assertRaises(ValueError, stem.types.Version, "12.3")
- self.assertRaises(ValueError, stem.types.Version, "1.-2.3")
-
- def test_comparison(self):
- """
- Tests comparision between Version instances.
- """
-
- # check for basic incrementing in each portion
- self.assert_version_is_greater("1.1.2.3-tag", "0.1.2.3-tag")
- self.assert_version_is_greater("0.2.2.3-tag", "0.1.2.3-tag")
- self.assert_version_is_greater("0.1.3.3-tag", "0.1.2.3-tag")
- self.assert_version_is_greater("0.1.2.4-tag", "0.1.2.3-tag")
- self.assert_version_is_greater("0.1.2.3-ugg", "0.1.2.3-tag")
- self.assert_version_is_equal("0.1.2.3-tag", "0.1.2.3-tag")
-
- # checks that a missing patch level equals zero
- self.assert_version_is_equal("0.1.2", "0.1.2.0")
- self.assert_version_is_equal("0.1.2-tag", "0.1.2.0-tag")
-
- # checks for missing patch or status
- self.assert_version_is_greater("0.1.2.3-tag", "0.1.2.3")
- self.assert_version_is_greater("0.1.2.3-tag", "0.1.2-tag")
- self.assert_version_is_greater("0.1.2.3-tag", "0.1.2")
-
- self.assert_version_is_equal("0.1.2.3", "0.1.2.3")
- self.assert_version_is_equal("0.1.2", "0.1.2")
-
- def test_nonversion_comparison(self):
- """
- Checks that we can be compared with other types.
- """
-
- test_version = stem.types.Version("0.1.2.3")
- self.assertNotEqual(test_version, None)
- self.assertTrue(test_version > None)
-
- self.assertNotEqual(test_version, 5)
- self.assertTrue(test_version > 5)
-
- def test_string(self):
- """
- Tests the Version -> string conversion.
- """
-
- # checks conversion with various numbers of arguments
-
- self.assert_string_matches("0.1.2.3-tag")
- self.assert_string_matches("0.1.2.3")
- self.assert_string_matches("0.1.2")
-
- def assert_versions_match(self, version, major, minor, micro, patch, status):
- """
- Asserts that the values for a types.Version instance match the given
- values.
- """
-
- self.assertEqual(version.major, major)
- self.assertEqual(version.minor, minor)
- self.assertEqual(version.micro, micro)
- self.assertEqual(version.patch, patch)
- self.assertEqual(version.status, status)
-
- def assert_version_is_greater(self, first_version, second_version):
- """
- Asserts that the parsed version of the first version is greate than the
- second (also checking the inverse).
- """
-
- version1 = stem.types.Version(first_version)
- version2 = stem.types.Version(second_version)
- self.assertEqual(version1 > version2, True)
- self.assertEqual(version1 < version2, False)
-
- def assert_version_is_equal(self, first_version, second_version):
- """
- Asserts that the parsed version of the first version equals the second.
- """
-
- version1 = stem.types.Version(first_version)
- version2 = stem.types.Version(second_version)
- self.assertEqual(version1, version2)
-
- def assert_string_matches(self, version):
- """
- Parses the given version string then checks that its string representation
- matches the input.
- """
-
- self.assertEqual(version, str(stem.types.Version(version)))
-
diff --git a/test/unit/version.py b/test/unit/version.py
new file mode 100644
index 0000000..2845ad3
--- /dev/null
+++ b/test/unit/version.py
@@ -0,0 +1,133 @@
+"""
+Unit tests for the stem.version.Version parsing and class.
+"""
+
+import unittest
+import stem.version
+
+class TestVerion(unittest.TestCase):
+ """
+ Tests methods and functions related to 'stem.version.Version'.
+ """
+
+ def test_parsing(self):
+ """
+ Tests parsing by the Version class constructor.
+ """
+
+ # valid versions with various number of compontents to the version
+ version = stem.version.Version("0.1.2.3-tag")
+ self.assert_versions_match(version, 0, 1, 2, 3, "tag")
+
+ version = stem.version.Version("0.1.2.3")
+ self.assert_versions_match(version, 0, 1, 2, 3, None)
+
+ version = stem.version.Version("0.1.2-tag")
+ self.assert_versions_match(version, 0, 1, 2, None, "tag")
+
+ version = stem.version.Version("0.1.2")
+ self.assert_versions_match(version, 0, 1, 2, None, None)
+
+ # checks an empty tag
+ version = stem.version.Version("0.1.2.3-")
+ self.assert_versions_match(version, 0, 1, 2, 3, "")
+
+ version = stem.version.Version("0.1.2-")
+ self.assert_versions_match(version, 0, 1, 2, None, "")
+
+ # checks invalid version strings
+ self.assertRaises(ValueError, stem.version.Version, "")
+ self.assertRaises(ValueError, stem.version.Version, "1.2.3.4nodash")
+ self.assertRaises(ValueError, stem.version.Version, "1.2.3.a")
+ self.assertRaises(ValueError, stem.version.Version, "1.2.a.4")
+ self.assertRaises(ValueError, stem.version.Version, "12.3")
+ self.assertRaises(ValueError, stem.version.Version, "1.-2.3")
+
+ def test_comparison(self):
+ """
+ Tests comparision between Version instances.
+ """
+
+ # check for basic incrementing in each portion
+ self.assert_version_is_greater("1.1.2.3-tag", "0.1.2.3-tag")
+ self.assert_version_is_greater("0.2.2.3-tag", "0.1.2.3-tag")
+ self.assert_version_is_greater("0.1.3.3-tag", "0.1.2.3-tag")
+ self.assert_version_is_greater("0.1.2.4-tag", "0.1.2.3-tag")
+ self.assert_version_is_greater("0.1.2.3-ugg", "0.1.2.3-tag")
+ self.assert_version_is_equal("0.1.2.3-tag", "0.1.2.3-tag")
+
+ # checks that a missing patch level equals zero
+ self.assert_version_is_equal("0.1.2", "0.1.2.0")
+ self.assert_version_is_equal("0.1.2-tag", "0.1.2.0-tag")
+
+ # checks for missing patch or status
+ self.assert_version_is_greater("0.1.2.3-tag", "0.1.2.3")
+ self.assert_version_is_greater("0.1.2.3-tag", "0.1.2-tag")
+ self.assert_version_is_greater("0.1.2.3-tag", "0.1.2")
+
+ self.assert_version_is_equal("0.1.2.3", "0.1.2.3")
+ self.assert_version_is_equal("0.1.2", "0.1.2")
+
+ def test_nonversion_comparison(self):
+ """
+ Checks that we can be compared with other types.
+ """
+
+ test_version = stem.version.Version("0.1.2.3")
+ self.assertNotEqual(test_version, None)
+ self.assertTrue(test_version > None)
+
+ self.assertNotEqual(test_version, 5)
+ self.assertTrue(test_version > 5)
+
+ def test_string(self):
+ """
+ Tests the Version -> string conversion.
+ """
+
+ # checks conversion with various numbers of arguments
+
+ self.assert_string_matches("0.1.2.3-tag")
+ self.assert_string_matches("0.1.2.3")
+ self.assert_string_matches("0.1.2")
+
+ def assert_versions_match(self, version, major, minor, micro, patch, status):
+ """
+ Asserts that the values for a types.Version instance match the given
+ values.
+ """
+
+ self.assertEqual(version.major, major)
+ self.assertEqual(version.minor, minor)
+ self.assertEqual(version.micro, micro)
+ self.assertEqual(version.patch, patch)
+ self.assertEqual(version.status, status)
+
+ def assert_version_is_greater(self, first_version, second_version):
+ """
+ Asserts that the parsed version of the first version is greate than the
+ second (also checking the inverse).
+ """
+
+ version1 = stem.version.Version(first_version)
+ version2 = stem.version.Version(second_version)
+ self.assertEqual(version1 > version2, True)
+ self.assertEqual(version1 < version2, False)
+
+ def assert_version_is_equal(self, first_version, second_version):
+ """
+ Asserts that the parsed version of the first version equals the second.
+ """
+
+ version1 = stem.version.Version(first_version)
+ version2 = stem.version.Version(second_version)
+ self.assertEqual(version1, version2)
+
+ def assert_string_matches(self, version):
+ """
+ Parses the given version string then checks that its string representation
+ matches the input.
+ """
+
+ self.assertEqual(version, str(stem.version.Version(version)))
+
1
0

26 Nov '11
commit e313bb35c827fdb75a382d7c5e837692baeead73
Author: Damian Johnson <atagar(a)torproject.org>
Date: Fri Nov 25 22:36:57 2011 -0800
Replacing protocolinfo lookups with ControlSocket
Replacing raw socket use in the protocolinfo lookup functions with the
ControlSocket class, and attaching it to the responses instead.
---
stem/connection.py | 41 ++++++++++++++---------------------------
1 files changed, 14 insertions(+), 27 deletions(-)
diff --git a/stem/connection.py b/stem/connection.py
index ba851a2..453b1d7 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -101,37 +101,24 @@ def _get_protocolinfo_impl(control_socket, connection_args, keep_alive):
connects the given socket and issues a PROTOCOLINFO query with it.
"""
- control_socket_file = control_socket.makefile()
- protocolinfo_response, raised_exc = None, None
-
try:
- # initiates connection
control_socket.connect(connection_args)
-
- # issues the PROTOCOLINFO query
- stem.socket.send_message(control_socket_file, "PROTOCOLINFO 1")
-
- protocolinfo_response = stem.socket.recv_message(control_socket_file)
- ProtocolInfoResponse.convert(protocolinfo_response)
+ control_socket = stem.socket.ControlSocket(control_socket)
except socket.error, exc:
- raised_exc = stem.socket.SocketError(exc)
- except (stem.socket.ProtocolError, stem.socket.SocketError), exc:
- raised_exc = exc
-
- control_socket_file.close() # done with the linked file
+ raise stem.socket.SocketError(exc)
- if not keep_alive or raised_exc:
- # shut down the socket we were using
- try: control_socket.shutdown(socket.SHUT_RDWR)
- except socket.error: pass
+ try:
+ control_socket.send("PROTOCOLINFO 1")
+ protocolinfo_response = control_socket.recv()
+ ProtocolInfoResponse.convert(protocolinfo_response)
+ if keep_alive: protocolinfo_response.socket = control_socket
+ else: control_socket.close()
+
+ return protocolinfo_response
+ except stem.socket.ControllerError, exc:
control_socket.close()
- else:
- # if we're keeping the socket open then attach it to the response
- protocolinfo_response.socket = control_socket
-
- if raised_exc: raise raised_exc
- else: return protocolinfo_response
+ raise exc
def _expand_cookie_path(cookie_path, pid_resolver, pid_resolution_arg):
"""
@@ -173,7 +160,7 @@ class ProtocolInfoResponse(stem.socket.ControlMessage):
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 collecion.
+ response, so all other values are None if undefined or empty if a collection.
Attributes:
protocol_version (int) - protocol version of the response
@@ -181,7 +168,7 @@ class ProtocolInfoResponse(stem.socket.ControlMessage):
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
- socket (socket.socket) - socket used to make the query
+ socket (stem.socket.ControlSocket) - socket used to make the query
"""
def convert(control_message):
1
0

26 Nov '11
commit 9a06ff17de582e834e2791c096bd9a51834a6d66
Author: Damian Johnson <atagar(a)torproject.org>
Date: Fri Nov 25 17:54:59 2011 -0800
Moving control message handling into stem.socket
Making a module for all low-level message handling with control sockets (ie,
pretty much all of the library work done so far). This includes most of the
code from the grab bag 'stem.types' module and the addition of a ControlSocket
class. The socket wrapper should greatly simplify upcoming parts of the
library.
---
run_tests.py | 12 +-
stem/__init__.py | 2 +-
stem/connection.py | 48 ++--
stem/socket.py | 679 +++++++++++++++++++++++++++++++++
stem/types.py | 530 -------------------------
test/integ/connection/protocolinfo.py | 10 +-
test/integ/socket/__init__.py | 6 +
test/integ/socket/control_message.py | 211 ++++++++++
test/integ/types/__init__.py | 6 -
test/integ/types/control_message.py | 210 ----------
test/runner.py | 8 +-
test/unit/connection/protocolinfo.py | 25 +-
test/unit/socket/__init__.py | 6 +
test/unit/socket/control_line.py | 163 ++++++++
test/unit/socket/control_message.py | 189 +++++++++
test/unit/types/__init__.py | 2 +-
test/unit/types/control_line.py | 163 --------
test/unit/types/control_message.py | 189 ---------
18 files changed, 1309 insertions(+), 1150 deletions(-)
diff --git a/run_tests.py b/run_tests.py
index 275c50e..ba74b66 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -13,13 +13,13 @@ import unittest
import StringIO
import test.runner
-import test.unit.types.control_message
-import test.unit.types.control_line
+import test.unit.socket.control_message
+import test.unit.socket.control_line
import test.unit.types.version
import test.unit.connection.protocolinfo
import test.unit.util.enum
import test.unit.util.system
-import test.integ.types.control_message
+import test.integ.socket.control_message
import test.integ.util.conf
import test.integ.util.system
import test.integ.connection.protocolinfo
@@ -32,15 +32,15 @@ OPT_EXPANDED = ["unit", "integ", "config=", "targets=", "help"]
DIVIDER = "=" * 70
# (name, class) tuples for all of our unit and integration tests
-UNIT_TESTS = (("stem.types.ControlMessage", test.unit.types.control_message.TestControlMessage),
- ("stem.types.ControlLine", test.unit.types.control_line.TestControlLine),
+UNIT_TESTS = (("stem.socket.ControlMessage", test.unit.socket.control_message.TestControlMessage),
+ ("stem.socket.ControlLine", test.unit.socket.control_line.TestControlLine),
("stem.types.Version", test.unit.types.version.TestVerion),
("stem.connection.ProtocolInfoResponse", test.unit.connection.protocolinfo.TestProtocolInfoResponse),
("stem.util.enum", test.unit.util.enum.TestEnum),
("stem.util.system", test.unit.util.system.TestSystem),
)
-INTEG_TESTS = (("stem.types.ControlMessage", test.integ.types.control_message.TestControlMessage),
+INTEG_TESTS = (("stem.socket.ControlMessage", test.integ.socket.control_message.TestControlMessage),
("stem.connection.ProtocolInfoResponse", test.integ.connection.protocolinfo.TestProtocolInfo),
("stem.util.conf", test.integ.util.conf.TestConf),
("stem.util.system", test.integ.util.system.TestSystem),
diff --git a/stem/__init__.py b/stem/__init__.py
index ac69776..d4ede49 100644
--- a/stem/__init__.py
+++ b/stem/__init__.py
@@ -4,5 +4,5 @@ Library for working with the tor process.
import stem.util # suppresses log handler warnings
-__all__ = ["process", "types"]
+__all__ = ["connection", "process", "socket", "types"]
diff --git a/stem/connection.py b/stem/connection.py
index 5fc06c1..ac548aa 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -14,11 +14,13 @@ ProtocolInfoResponse - Reply from a PROTOCOLINFO query.
+- convert - parses a ControlMessage, turning it into a ProtocolInfoResponse
"""
+from __future__ import absolute_import
import Queue
import socket
import logging
import threading
+import stem.socket
import stem.types
import stem.util.enum
import stem.util.system
@@ -55,8 +57,8 @@ def get_protocolinfo_by_port(control_addr = "127.0.0.1", control_port = 9051, ke
ProtocolInfoResponse with the response given by the tor process
Raises:
- stem.types.ProtocolError if the PROTOCOLINFO response is malformed
- stem.types.SocketError if problems arise in establishing or using the
+ stem.socket.ProtocolError if the PROTOCOLINFO response is malformed
+ stem.socket.SocketError if problems arise in establishing or using the
socket
"""
@@ -80,8 +82,8 @@ def get_protocolinfo_by_socket(socket_path = "/var/run/tor/control", keep_alive
open if True, closes otherwise
Raises:
- stem.types.ProtocolError if the PROTOCOLINFO response is malformed
- stem.types.SocketError if problems arise in establishing or using the
+ stem.socket.ProtocolError if the PROTOCOLINFO response is malformed
+ stem.socket.SocketError if problems arise in establishing or using the
socket
"""
@@ -107,13 +109,13 @@ def _get_protocolinfo_impl(control_socket, connection_args, keep_alive):
control_socket.connect(connection_args)
# issues the PROTOCOLINFO query
- stem.types.write_message(control_socket_file, "PROTOCOLINFO 1")
+ stem.socket.send_message(control_socket_file, "PROTOCOLINFO 1")
- protocolinfo_response = stem.types.read_message(control_socket_file)
+ protocolinfo_response = stem.socket.recv_message(control_socket_file)
ProtocolInfoResponse.convert(protocolinfo_response)
except socket.error, exc:
- raised_exc = stem.types.SocketError(exc)
- except (stem.types.ProtocolError, stem.types.SocketError), exc:
+ raised_exc = stem.socket.SocketError(exc)
+ except (stem.socket.ProtocolError, stem.socket.SocketError), exc:
raised_exc = exc
control_socket_file.close() # done with the linked file
@@ -159,7 +161,7 @@ def _expand_cookie_path(cookie_path, pid_resolver, pid_resolution_arg):
return cookie_path
-class ProtocolInfoResponse(stem.types.ControlMessage):
+class ProtocolInfoResponse(stem.socket.ControlMessage):
"""
Version one PROTOCOLINFO query response.
@@ -188,20 +190,20 @@ class ProtocolInfoResponse(stem.types.ControlMessage):
ProtocolInfoResponse.
Arguments:
- control_message (stem.types.ControlMessage) -
+ control_message (stem.socket.ControlMessage) -
message to be parsed as a PROTOCOLINFO reply
Raises:
- stem.types.ProtocolError the message isn't a proper PROTOCOLINFO response
+ stem.socket.ProtocolError the message isn't a proper PROTOCOLINFO response
TypeError if argument isn't a ControlMessage
"""
- if isinstance(control_message, stem.types.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.types.ControlMessage instances")
+ raise TypeError("Only able to convert stem.socket.ControlMessage instances")
convert = staticmethod(convert)
@@ -222,7 +224,7 @@ class ProtocolInfoResponse(stem.types.ControlMessage):
# sanity check that we're a PROTOCOLINFO response
if not list(self)[0].startswith("PROTOCOLINFO"):
msg = "Message is not a PROTOCOLINFO response"
- raise stem.types.ProtocolError(msg)
+ raise stem.socket.ProtocolError(msg)
for line in self:
if line == "OK": break
@@ -237,13 +239,13 @@ class ProtocolInfoResponse(stem.types.ControlMessage):
if line.is_empty():
msg = "PROTOCOLINFO response's initial line is missing the protocol version: %s" % line
- raise stem.types.ProtocolError(msg)
+ raise stem.socket.ProtocolError(msg)
piversion = line.pop()
if not piversion.isdigit():
msg = "PROTOCOLINFO response version is non-numeric: %s" % line
- raise stem.types.ProtocolError(msg)
+ raise stem.socket.ProtocolError(msg)
self.protocol_version = int(piversion)
@@ -264,7 +266,7 @@ class ProtocolInfoResponse(stem.types.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.types.ProtocolError(msg)
+ raise stem.socket.ProtocolError(msg)
for method in line.pop_mapping()[1].split(","):
if method == "NULL":
@@ -295,14 +297,14 @@ class ProtocolInfoResponse(stem.types.ControlMessage):
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.types.ProtocolError(msg)
+ raise stem.socket.ProtocolError(msg)
torversion = line.pop_mapping(True)[1]
try:
self.tor_version = stem.types.Version(torversion)
except ValueError, exc:
- raise stem.types.ProtocolError(exc)
+ raise stem.socket.ProtocolError(exc)
else:
LOGGER.debug("unrecognized PROTOCOLINFO line type '%s', ignoring entry: %s" % (line_type, line))
@@ -358,7 +360,7 @@ class ControlConnection:
whenever we receive an event from the control socket.
Arguments:
- event_message (stem.types.ControlMessage) -
+ event_message (stem.socket.ControlMessage) -
message received from the control socket
"""
@@ -372,7 +374,7 @@ class ControlConnection:
message (str) - message to be sent to the control socket
Returns:
- stem.types.ControlMessage with the response from the control socket
+ stem.socket.ControlMessage with the response from the control socket
"""
# makes sure that the message ends with a CRLF
@@ -413,7 +415,7 @@ class ControlConnection:
while self.is_running():
try:
# TODO: this raises a SocketClosed when... well, the socket is closed
- control_message = stem.types.read_message(self._control_socket_file)
+ control_message = stem.socket.recv_message(self._control_socket_file)
if control_message.content()[-1][0] == "650":
# adds this to the event queue and wakes up the handler
@@ -425,7 +427,7 @@ class ControlConnection:
else:
# TODO: figure out a good method for terminating the socket thread
self._reply_queue.put(control_message)
- except stem.types.ProtocolError, exc:
+ except stem.socket.ProtocolError, exc:
LOGGER.error("Error reading control socket message: %s" % exc)
# TODO: terminate?
diff --git a/stem/socket.py b/stem/socket.py
new file mode 100644
index 0000000..451b535
--- /dev/null
+++ b/stem/socket.py
@@ -0,0 +1,679 @@
+"""
+Supports message based communication with sockets speaking the tor control
+protocol. This lets users send messages as basic strings and receive responses
+as instances of the ControlMessage class.
+
+ControllerError - Base exception raised when using the controller.
+ |- ProtocolError - Malformed socket data.
+ +- SocketError - Communication with the socket failed.
+ +- SocketClosed - Socket has been shut down.
+
+ControlSocket - Socket wrapper that speaks the tor control protocol.
+ |- send - sends a message to the socket
+ |- recv - receives a ControlMessage from the socket
+ |- is_alive - reports if the socket is known to be closed
+ +- close - shuts down the socket
+
+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
+ |- 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.
+"""
+
+from __future__ import absolute_import
+import re
+import socket
+import logging
+import threading
+
+LOGGER = logging.getLogger("stem.socket")
+
+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 which looks to be dead
+# code
+
+CONTROL_ESCAPES = {r"\\": "\\", r"\"": "\"", r"\'": "'",
+ r"\r": "\r", r"\n": "\n", r"\t": "\t"}
+
+class ControllerError(Exception):
+ "Base error for controller communication issues."
+
+class ProtocolError(ControllerError):
+ "Malformed content from the control socket."
+ pass
+
+class SocketError(ControllerError):
+ "Error arose while communicating with the control socket."
+ pass
+
+class SocketClosed(SocketError):
+ "Control socket was closed before completing the message."
+ pass
+
+class ControlSocket:
+ """
+ Wrapper for a socket connection that speaks the Tor control protocol. To the
+ better part this transparently handles the formatting for sending and
+ receiving complete messages. All methods are thread safe.
+ """
+
+ def __init__(self, control_socket):
+ """
+ Constructs as a wrapper around an established socket connection. Further
+ interaction with the raw socket is discouraged.
+
+ Arguments:
+ control_socket (socket.socket) - established tor control socket
+ """
+
+ self._socket = control_socket
+ self._socket_file = control_socket.makefile()
+ self._is_alive = True
+
+ # Tracks sending and receiving separately. This should be safe, and doing
+ # so prevents deadlock where we block writes because we're waiting to read
+ # a message that isn't coming.
+
+ self._send_cond = threading.Condition()
+ self._recv_cond = threading.Condition()
+
+ def send(self, message, raw = False):
+ """
+ Formats and sends a message to the control socket. For more information see
+ the stem.socket.send_message function.
+
+ Arguments:
+ message (str) - message to be formatted and sent to the socket
+ raw (bool) - leaves the message formatting untouched, passing it to
+ the socket as-is
+
+ Raises:
+ stem.socket.SocketError if a problem arises in using the socket
+ stem.socket.SocketClosed if the socket is shut down
+ """
+
+ self._send_cond.acquire()
+
+ try:
+ if not self.is_alive(): raise SocketClosed()
+ send_message(self._socket_file, message, raw)
+ except SocketClosed, exc:
+ # if send_message raises a SocketClosed then we should properly shut
+ # everything down
+ if self.is_alive(): self.close()
+ raise exc
+ finally:
+ self._send_cond.release()
+
+ def recv(self):
+ """
+ Receives a message from the control socket, blocking until we've received
+ one. For more information see the stem.socket.recv_message function.
+
+ Returns:
+ stem.socket.ControlMessage for the message received
+
+ Raises:
+ stem.socket.ProtocolError the content from the socket is malformed
+ stem.socket.SocketClosed if the socket closes before we receive a
+ complete message
+ """
+
+ self._recv_cond.acquire()
+
+ try:
+ if not self.is_alive(): raise SocketClosed()
+ return recv_message(self._socket_file)
+ except SocketClosed, exc:
+ # if recv_message raises a SocketClosed then we should properly shut
+ # everything down
+ if self.is_alive(): self.close()
+ raise exc
+ finally:
+ self._recv_cond.release()
+
+ def is_alive(self):
+ """
+ Checks if the socket is known to be closed. We won't be aware if it is
+ until we either use it or have explicitily shut it down.
+
+ Returns:
+ bool that's True if we're known to be shut down and False otherwise
+ """
+
+ return self._is_alive
+
+ def close(self):
+ """
+ Shuts down the socket. If it's already closed then this is a no-op.
+ """
+
+ # we need both locks for this
+ self._send_cond.acquire()
+ self._recv_cond.acquire()
+
+ # if we haven't yet established a connection then this raises an error
+ # socket.error: [Errno 107] Transport endpoint is not connected
+ try: self._socket.shutdown(socket.SHUT_RDWR)
+ except socket.error: pass
+
+ # Suppressing unexpected exceptions from close. For instance, if the
+ # socket's file has already been closed then with python 2.7 that raises
+ # with...
+ # error: [Errno 32] Broken pipe
+
+ try: self._socket.close()
+ except: pass
+
+ try: self._socket_file.close()
+ except: pass
+
+ self._is_alive = False
+
+ self._send_cond.release()
+ self._recv_cond.release()
+
+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 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 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
+ """
+
+ try:
+ self._remainder_lock.acquire()
+ next_entry, remainder = _parse_entry(self._remainder, quoted, escaped)
+ self._remainder = remainder
+ return next_entry
+ finally:
+ self._remainder_lock.release()
+
+ 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
+ """
+
+ try:
+ self._remainder_lock.acquire()
+ 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)
+ finally:
+ self._remainder_lock.release()
+
+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
+ single verses multiline messages. Neither message type should contain an
+ ending newline (if so it'll be treated as a multi-line message with a blank
+ line at the end). If the message doesn't contain a newline then it's sent
+ as...
+
+ <message>\r\n
+
+ and if it does contain newlines then it's split on \n and sent as...
+
+ +<line 1>\r\n
+ <line 2>\r\n
+ <line 3>\r\n
+ .\r\n
+
+ Arguments:
+ control_file (file) - file derived from the control socket (see the
+ socket's makefile() method for more information)
+ message (str) - message to be sent on the control socket
+ raw (bool) - leaves the message formatting untouched, passing it
+ to the socket as-is
+
+ Raises:
+ stem.socket.SocketError if a problem arises in using the socket
+ """
+
+ if not raw: message = send_formatting(message)
+
+ try:
+ log_message = message.replace("\r\n", "\n").rstrip()
+
+ # starts with a newline if this is a multi-line message (more readable)
+ if "\n" in log_message: log_message = "\n" + log_message
+
+ LOGGER.debug("Sending: " + log_message)
+
+ control_file.write(message)
+ control_file.flush()
+ except socket.error, exc:
+ LOGGER.info("Failed to send message: %s" % exc)
+ raise SocketError(exc)
+ except AttributeError:
+ # if the control_file has been closed then flush will receive:
+ # AttributeError: 'NoneType' object has no attribute 'sendall'
+
+ LOGGER.info("Failed to send message: file has been closed")
+ raise SocketClosed("file has been closed")
+
+def recv_message(control_file):
+ """
+ Pulls from a control socket until we either have a complete message or
+ encounter a problem.
+
+ Arguments:
+ control_file (file) - file derived from the control socket (see the
+ socket's makefile() method for more information)
+
+ Returns:
+ stem.socket.ControlMessage read from the socket
+
+ Raises:
+ stem.socket.ProtocolError the content from the socket is malformed
+ stem.socket.SocketClosed if the socket closes before we receive a complete
+ message
+ """
+
+ parsed_content, raw_content = [], ""
+
+ while True:
+ try: line = control_file.readline()
+ except AttributeError:
+ # if the control_file has been closed then we will receive:
+ # AttributeError: 'NoneType' object has no attribute 'recv'
+
+ LOGGER.warn("SocketClosed: socket file has been closed")
+ raise SocketClosed("socket file has been closed")
+ except socket.error, exc:
+ # when disconnected we get...
+ # socket.error: [Errno 107] Transport endpoint is not connected
+
+ LOGGER.warn("SocketClosed: received an exception (%s)" % exc)
+ raise SocketClosed(exc)
+
+ raw_content += line
+
+ # Parses the tor control lines. These are of the form...
+ # <status code><divider><content>\r\n
+
+ if len(line) == 0:
+ # if the socket is disconnected then the readline() method will provide
+ # empty content
+
+ LOGGER.warn("SocketClosed: empty socket content")
+ raise SocketClosed("Received empty socket content.")
+ elif len(line) < 4:
+ LOGGER.warn("ProtocolError: line too short (%s)" % line)
+ raise ProtocolError("Badly formatted reply line: too short")
+ elif not re.match(r'^[a-zA-Z0-9]{3}[-+ ]', line):
+ LOGGER.warn("ProtocolError: malformed status code/divider (%s)" % line)
+ raise ProtocolError("Badly formatted reply line: beginning is malformed")
+ elif not line.endswith("\r\n"):
+ LOGGER.warn("ProtocolError: no CRLF linebreak (%s)" % line)
+ raise ProtocolError("All lines should end with CRLF")
+
+ line = line[:-2] # strips off the CRLF
+ status_code, divider, content = line[:3], line[3], line[4:]
+
+ if divider == "-":
+ # mid-reply line, keep pulling for more content
+ parsed_content.append((status_code, divider, content))
+ elif divider == " ":
+ # end of the message, return the message
+ parsed_content.append((status_code, divider, content))
+
+ # replacing the \r\n newline endings and the ending newline since it
+ # leads to more readable log messages
+ log_message = raw_content.replace("\r\n", "\n").rstrip()
+
+ # starts with a newline if this is a multi-line message (more readable)
+ if "\n" in log_message: log_message = "\n" + log_message
+
+ LOGGER.debug("Received: " + log_message)
+
+ return 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
+
+ while True:
+ try: line = control_file.readline()
+ except socket.error, exc: raise SocketClosed(exc)
+
+ raw_content += line
+
+ if not line.endswith("\r\n"):
+ LOGGER.warn("ProtocolError: no CRLF linebreak for data entry (%s)" % line)
+ raise ProtocolError("All lines should end with CRLF")
+ elif line == ".\r\n":
+ break # data block termination
+
+ line = line[:-2] # strips off the CRLF
+
+ # lines starting with a period are escaped by a second period (as per
+ # section 2.4 of the control-spec)
+ if line.startswith(".."): line = line[1:]
+
+ # appends to previous content, using a newline rather than CRLF
+ # separator (more conventional for multi-line string content outside
+ # the windows world)
+
+ content += "\n" + line
+
+ parsed_content.append((status_code, divider, content))
+ else:
+ # this should never be reached due to the prefix regex, but might as well
+ # be safe...
+ LOGGER.warn("ProtocolError: unrecognized divider type (%s)" % line)
+ raise ProtocolError("Unrecognized type '%s': %s" % (divider, line))
+
+def send_formatting(message):
+ """
+ Performs the formatting expected from sent control messages. For more
+ information see the stem.socket.send_message function.
+
+ Arguments:
+ message (str) - message to be formatted
+
+ Returns:
+ str of the message wrapped by the formatting expected from controllers
+ """
+
+ # From control-spec section 2.2...
+ # Command = Keyword OptArguments CRLF / "+" Keyword OptArguments CRLF CmdData
+ # Keyword = 1*ALPHA
+ # OptArguments = [ SP *(SP / VCHAR) ]
+ #
+ # A command is either a single line containing a Keyword and arguments, or a
+ # multiline command whose initial keyword begins with +, and whose data
+ # section ends with a single "." on a line of its own.
+
+ # if we already have \r\n entries then standardize on \n to start with
+ message = message.replace("\r\n", "\n")
+
+ if "\n" in message:
+ return "+%s\r\n.\r\n" % message.replace("\n", "\r\n")
+ else:
+ return message + "\r\n"
+
diff --git a/stem/types.py b/stem/types.py
index 0bc3c45..3d62582 100644
--- a/stem/types.py
+++ b/stem/types.py
@@ -2,28 +2,6 @@
Class representations for a variety of tor objects. These are most commonly
return values rather than being instantiated by users directly.
-ControllerError - Base exception raised when using the controller.
- |- ProtocolError - Malformed socket data.
- |- SocketError - Socket used for controller communication errored.
- +- SocketClosed - Socket terminated.
-
-write_message - Writes a message to a control socket.
-format_write_message - Performs the formatting expected from sent messages.
-read_message - Reads a ControlMessage from a control socket.
-ControlMessage - Message 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
- |- pop - removes and returns the next entry
- +- pop_mapping - removes and returns the next entry as a KEY=VALUE mapping
-
Version - Tor versioning information.
|- __str__ - string representation
+- __cmp__ - compares with another Version
@@ -36,514 +14,6 @@ import threading
LOGGER = logging.getLogger("stem")
-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 which looks to be dead
-# code
-
-CONTROL_ESCAPES = {r"\\": "\\", r"\"": "\"", r"\'": "'",
- r"\r": "\r", r"\n": "\n", r"\t": "\t"}
-
-class ControllerError(Exception):
- "Base error for controller communication issues."
-
-class ProtocolError(ControllerError):
- "Malformed content from the control socket."
- pass
-
-class SocketError(ControllerError):
- "Error arose while communicating with the control socket."
- pass
-
-class SocketClosed(SocketError):
- "Control socket was closed before completing the message."
- pass
-
-def write_message(control_file, message, raw = False):
- """
- Sends a message to the control socket, adding the expected formatting for
- single verses multiline messages. Neither message type should contain an
- ending newline (if so it'll be treated as a multi-line message with a blank
- line at the end). If the message doesn't contain a newline then it's sent
- as...
-
- <message>\r\n
-
- and if it does contain newlines then it's split on \n and sent as...
-
- +<line 1>\r\n
- <line 2>\r\n
- <line 3>\r\n
- .\r\n
-
- Arguments:
- control_file (file) - file derived from the control socket (see the
- socket's makefile() method for more information)
- message (str) - message to be sent on the control socket
- raw (bool) - leaves the message formatting untouched, passing it
- to the socket as-is
-
- Raises:
- SocketError if a problem arises in using the socket
- """
-
- if not raw: message = format_write_message(message)
-
- try:
- log_message = message.replace("\r\n", "\n").rstrip()
-
- # starts with a newline if this is a multi-line message (more readable)
- if "\n" in log_message: log_message = "\n" + log_message
-
- LOGGER.debug("Sending: " + log_message)
-
- control_file.write(message)
- control_file.flush()
- except socket.error, exc:
- LOGGER.info("Failed to send message: %s" % exc)
- raise SocketError(exc)
- except AttributeError:
- # This happens after the file's close() method has been called, the flush
- # causing...
- # AttributeError: 'NoneType' object has no attribute 'sendall'
-
- LOGGER.info("Failed to send message: file has been closed")
- raise SocketError("file has been closed")
-
-def format_write_message(message):
- """
- Performs the formatting expected of control messages (for more information
- see the write_message function).
-
- Arguments:
- message (str) - message to be formatted
-
- Returns:
- str of the message wrapped by the formatting expected from controllers
- """
-
- # From 'Commands from controller to Tor' (section 2.2) of the control spec...
- #
- # Command = Keyword OptArguments CRLF / "+" Keyword OptArguments CRLF CmdData
- # Keyword = 1*ALPHA
- # OptArguments = [ SP *(SP / VCHAR) ]
- #
- # A command is either a single line containing a Keyword and arguments, or a
- # multiline command whose initial keyword begins with +, and whose data
- # section ends with a single "." on a line of its own.
-
- if "\n" in message:
- return "+%s\r\n.\r\n" % message.replace("\n", "\r\n")
- else:
- return message + "\r\n"
-
-def read_message(control_file):
- """
- Pulls from a control socket until we either have a complete message or
- encounter a problem.
-
- Arguments:
- control_file (file) - file derived from the control socket (see the
- socket's makefile() method for more information)
-
- Returns:
- stem.types.ControlMessage read from the socket
-
- Raises:
- ProtocolError the content from the socket is malformed
- SocketClosed if the socket closes before we receive a complete message
- """
-
- parsed_content, raw_content = [], ""
-
- while True:
- try: line = control_file.readline()
- except AttributeError, exc:
- # if the control_file has been closed then we will receive:
- # AttributeError: 'NoneType' object has no attribute 'recv'
-
- LOGGER.warn("SocketClosed: socket file has been closed")
- raise SocketClosed("socket file has been closed")
- except socket.error, exc:
- LOGGER.warn("SocketClosed: received an exception (%s)" % exc)
- raise SocketClosed(exc)
-
- raw_content += line
-
- # Parses the tor control lines. These are of the form...
- # <status code><divider><content>\r\n
-
- if len(line) == 0:
- # if the socket is disconnected then the readline() method will provide
- # empty content
-
- LOGGER.warn("SocketClosed: empty socket content")
- raise SocketClosed("Received empty socket content.")
- elif len(line) < 4:
- LOGGER.warn("ProtocolError: line too short (%s)" % line)
- raise ProtocolError("Badly formatted reply line: too short")
- elif not re.match(r'^[a-zA-Z0-9]{3}[-+ ]', line):
- LOGGER.warn("ProtocolError: malformed status code/divider (%s)" % line)
- raise ProtocolError("Badly formatted reply line: beginning is malformed")
- elif not line.endswith("\r\n"):
- LOGGER.warn("ProtocolError: no CRLF linebreak (%s)" % line)
- raise ProtocolError("All lines should end with CRLF")
-
- line = line[:-2] # strips off the CRLF
- status_code, divider, content = line[:3], line[3], line[4:]
-
- if divider == "-":
- # mid-reply line, keep pulling for more content
- parsed_content.append((status_code, divider, content))
- elif divider == " ":
- # end of the message, return the message
- parsed_content.append((status_code, divider, content))
-
- # replacing the \r\n newline endings and the ending newline since it
- # leads to more readable log messages
- log_message = raw_content.replace("\r\n", "\n").rstrip()
-
- # starts with a newline if this is a multi-line message (more readable)
- if "\n" in log_message: log_message = "\n" + log_message
-
- LOGGER.debug("Received: " + log_message)
-
- return 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
-
- while True:
- try: line = control_file.readline()
- except socket.error, exc: raise SocketClosed(exc)
-
- raw_content += line
-
- if not line.endswith("\r\n"):
- LOGGER.warn("ProtocolError: no CRLF linebreak for data entry (%s)" % line)
- raise ProtocolError("All lines should end with CRLF")
- elif line == ".\r\n":
- break # data block termination
-
- line = line[:-2] # strips off the CRLF
-
- # lines starting with a period are escaped by a second period (as per
- # section 2.4 of the control-spec)
- if line.startswith(".."): line = line[1:]
-
- # appends to previous content, using a newline rather than CRLF
- # separator (more conventional for multi-line string content outside
- # the windows world)
-
- content += "\n" + line
-
- parsed_content.append((status_code, divider, content))
- else:
- # this should never be reached due to the prefix regex, but might as well
- # be safe...
- LOGGER.warn("ProtocolError: unrecognized divider type (%s)" % line)
- raise ProtocolError("Unrecognized type '%s': %s" % (divider, line))
-
-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 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 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
- """
-
- try:
- self._remainder_lock.acquire()
- next_entry, remainder = _parse_entry(self._remainder, quoted, escaped)
- self._remainder = remainder
- return next_entry
- finally:
- self._remainder_lock.release()
-
- 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
- """
-
- try:
- self._remainder_lock.acquire()
- 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)
- finally:
- self._remainder_lock.release()
-
-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)
-
class Version:
"""
Comparable tor version, as per the 'new version' of the version-spec...
diff --git a/test/integ/connection/protocolinfo.py b/test/integ/connection/protocolinfo.py
index e2e9a7b..71d2d6c 100644
--- a/test/integ/connection/protocolinfo.py
+++ b/test/integ/connection/protocolinfo.py
@@ -6,7 +6,7 @@ related functions.
import unittest
import test.runner
-import stem.types
+import stem.socket
import stem.connection
import stem.util.system
@@ -31,8 +31,8 @@ class TestProtocolInfo(unittest.TestCase):
control_socket = runner.get_tor_socket(False)
control_socket_file = control_socket.makefile()
- stem.types.write_message(control_socket_file, "PROTOCOLINFO 1")
- protocolinfo_response = stem.types.read_message(control_socket_file)
+ stem.socket.send_message(control_socket_file, "PROTOCOLINFO 1")
+ protocolinfo_response = stem.socket.recv_message(control_socket_file)
stem.connection.ProtocolInfoResponse.convert(protocolinfo_response)
# according to the control spec the following _could_ differ or be
@@ -75,7 +75,7 @@ class TestProtocolInfo(unittest.TestCase):
self.assert_protocolinfo_attr(protocolinfo_response, connection_type)
else:
# we don't have a control port
- self.assertRaises(stem.types.SocketError, stem.connection.get_protocolinfo_by_port, "127.0.0.1", test.runner.CONTROL_PORT)
+ self.assertRaises(stem.socket.SocketError, stem.connection.get_protocolinfo_by_port, "127.0.0.1", test.runner.CONTROL_PORT)
stem.util.system.CALL_MOCKING = None
@@ -94,7 +94,7 @@ class TestProtocolInfo(unittest.TestCase):
self.assert_protocolinfo_attr(protocolinfo_response, connection_type)
else:
# we don't have a control socket
- self.assertRaises(stem.types.SocketError, stem.connection.get_protocolinfo_by_socket, test.runner.CONTROL_SOCKET_PATH)
+ self.assertRaises(stem.socket.SocketError, stem.connection.get_protocolinfo_by_socket, test.runner.CONTROL_SOCKET_PATH)
stem.util.system.CALL_MOCKING = None
diff --git a/test/integ/socket/__init__.py b/test/integ/socket/__init__.py
new file mode 100644
index 0000000..d01630e
--- /dev/null
+++ b/test/integ/socket/__init__.py
@@ -0,0 +1,6 @@
+"""
+Integration tests for stem.socket.
+"""
+
+__all__ = ["control_message"]
+
diff --git a/test/integ/socket/control_message.py b/test/integ/socket/control_message.py
new file mode 100644
index 0000000..bbcd222
--- /dev/null
+++ b/test/integ/socket/control_message.py
@@ -0,0 +1,211 @@
+"""
+Integration tests for the stem.socket.ControlMessage class.
+"""
+
+import re
+import socket
+import unittest
+
+import stem.socket
+import stem.types
+import test.runner
+
+class TestControlMessage(unittest.TestCase):
+ """
+ Exercises the 'stem.socket.ControlMessage' class with an actual tor instance.
+ """
+
+ def test_unestablished_socket(self):
+ """
+ Checks message parsing when we have a valid but unauthenticated socket.
+ """
+
+ control_socket = test.runner.get_runner().get_tor_socket(False)
+ if not control_socket: self.skipTest("(no control socket)")
+ control_socket_file = control_socket.makefile()
+
+ # If an unauthenticated connection gets a message besides AUTHENTICATE or
+ # PROTOCOLINFO then tor will give an 'Authentication required.' message and
+ # hang up.
+
+ stem.socket.send_message(control_socket_file, "GETINFO version")
+
+ auth_required_response = stem.socket.recv_message(control_socket_file)
+ self.assertEquals("Authentication required.", str(auth_required_response))
+ self.assertEquals(["Authentication required."], list(auth_required_response))
+ self.assertEquals("514 Authentication required.\r\n", auth_required_response.raw_content())
+ self.assertEquals([("514", " ", "Authentication required.")], auth_required_response.content())
+
+ # The socket's broken but doesn't realize it yet. Send another message and
+ # it should fail with a closed exception. With a control port we won't get
+ # an error until we read from the socket. However, with a control socket
+ # the write will cause a SocketError.
+
+ try:
+ stem.socket.send_message(control_socket_file, "GETINFO version")
+ except: pass
+
+ self.assertRaises(stem.socket.SocketClosed, stem.socket.recv_message, control_socket_file)
+
+ # Additional socket usage should fail, and pulling more responses will fail
+ # with more closed exceptions.
+
+ self.assertRaises(stem.socket.SocketError, stem.socket.send_message, control_socket_file, "GETINFO version")
+ self.assertRaises(stem.socket.SocketClosed, stem.socket.recv_message, control_socket_file)
+ self.assertRaises(stem.socket.SocketClosed, stem.socket.recv_message, control_socket_file)
+ self.assertRaises(stem.socket.SocketClosed, stem.socket.recv_message, control_socket_file)
+
+ # The socket connection is already broken so calling close shouldn't have
+ # an impact.
+
+ control_socket.close()
+ self.assertRaises(stem.socket.SocketError, stem.socket.send_message, control_socket_file, "GETINFO version")
+ self.assertRaises(stem.socket.SocketClosed, stem.socket.recv_message, control_socket_file)
+
+ # Tries again with the file explicitely closed. In python 2.7 the close
+ # call will raise...
+ # error: [Errno 32] Broken pipe
+
+ try: control_socket_file.close()
+ except: pass
+
+ self.assertRaises(stem.socket.SocketError, stem.socket.send_message, control_socket_file, "GETINFO version")
+
+ # receives: stem.socket.SocketClosed: socket file has been closed
+ self.assertRaises(stem.socket.SocketClosed, stem.socket.recv_message, control_socket_file)
+
+ def test_invalid_command(self):
+ """
+ Parses the response for a command which doesn't exist.
+ """
+
+ control_socket = test.runner.get_runner().get_tor_socket()
+ if not control_socket: self.skipTest("(no control socket)")
+ control_socket_file = control_socket.makefile()
+
+ stem.socket.send_message(control_socket_file, "blarg")
+ unrecognized_command_response = stem.socket.recv_message(control_socket_file)
+ self.assertEquals('Unrecognized command "blarg"', str(unrecognized_command_response))
+ self.assertEquals(['Unrecognized command "blarg"'], list(unrecognized_command_response))
+ self.assertEquals('510 Unrecognized command "blarg"\r\n', unrecognized_command_response.raw_content())
+ self.assertEquals([('510', ' ', 'Unrecognized command "blarg"')], unrecognized_command_response.content())
+
+ control_socket.close()
+ control_socket_file.close()
+
+ def test_invalid_getinfo(self):
+ """
+ Parses the response for a GETINFO query which doesn't exist.
+ """
+
+ control_socket = test.runner.get_runner().get_tor_socket()
+ if not control_socket: self.skipTest("(no control socket)")
+ control_socket_file = control_socket.makefile()
+
+ stem.socket.send_message(control_socket_file, "GETINFO blarg")
+ unrecognized_key_response = stem.socket.recv_message(control_socket_file)
+ self.assertEquals('Unrecognized key "blarg"', str(unrecognized_key_response))
+ self.assertEquals(['Unrecognized key "blarg"'], list(unrecognized_key_response))
+ self.assertEquals('552 Unrecognized key "blarg"\r\n', unrecognized_key_response.raw_content())
+ self.assertEquals([('552', ' ', 'Unrecognized key "blarg"')], unrecognized_key_response.content())
+
+ control_socket.close()
+ control_socket_file.close()
+
+ def test_getinfo_config_file(self):
+ """
+ Parses the 'GETINFO config-file' response.
+ """
+
+ runner = test.runner.get_runner()
+ torrc_dst = runner.get_torrc_path()
+
+ control_socket = runner.get_tor_socket()
+ if not control_socket: self.skipTest("(no control socket)")
+ control_socket_file = control_socket.makefile()
+
+ stem.socket.send_message(control_socket_file, "GETINFO config-file")
+ config_file_response = stem.socket.recv_message(control_socket_file)
+ self.assertEquals("config-file=%s\nOK" % torrc_dst, str(config_file_response))
+ self.assertEquals(["config-file=%s" % torrc_dst, "OK"], list(config_file_response))
+ self.assertEquals("250-config-file=%s\r\n250 OK\r\n" % torrc_dst, config_file_response.raw_content())
+ self.assertEquals([("250", "-", "config-file=%s" % torrc_dst), ("250", " ", "OK")], config_file_response.content())
+
+ control_socket.close()
+ control_socket_file.close()
+
+ def test_getinfo_config_text(self):
+ """
+ Parses the 'GETINFO config-text' response.
+ """
+
+ if stem.process.get_tor_version() < stem.types.REQ_GETINFO_CONFIG_TEXT:
+ self.skipTest("(requires %s)" % stem.types.REQ_GETINFO_CONFIG_TEXT)
+
+ # We can't be certain of the order, and there may be extra config-text
+ # entries as per...
+ # https://trac.torproject.org/projects/tor/ticket/2362
+ #
+ # so we'll just check that the response is a superset of our config
+
+ runner = test.runner.get_runner()
+ torrc_contents = []
+
+ for line in runner.get_torrc_contents().split("\n"):
+ line = line.strip()
+
+ if line and not line.startswith("#"):
+ torrc_contents.append(line)
+
+ control_socket = runner.get_tor_socket()
+ if not control_socket: self.skipTest("(no control socket)")
+ control_socket_file = control_socket.makefile()
+
+ stem.socket.send_message(control_socket_file, "GETINFO config-text")
+ config_text_response = stem.socket.recv_message(control_socket_file)
+
+ # the response should contain two entries, the first being a data response
+ self.assertEqual(2, len(list(config_text_response)))
+ self.assertEqual("OK", list(config_text_response)[1])
+ self.assertEqual(("250", " ", "OK"), config_text_response.content()[1])
+ self.assertTrue(config_text_response.raw_content().startswith("250+config-text=\r\n"))
+ self.assertTrue(config_text_response.raw_content().endswith("\r\n.\r\n250 OK\r\n"))
+ self.assertTrue(str(config_text_response).startswith("config-text=\n"))
+ self.assertTrue(str(config_text_response).endswith("\nOK"))
+
+ for torrc_entry in torrc_contents:
+ self.assertTrue("\n%s\n" % torrc_entry in str(config_text_response))
+ self.assertTrue(torrc_entry in list(config_text_response)[0])
+ self.assertTrue("%s\r\n" % torrc_entry in config_text_response.raw_content())
+ self.assertTrue("%s" % torrc_entry in config_text_response.content()[0][2])
+
+ control_socket.close()
+ control_socket_file.close()
+
+ def test_bw_event(self):
+ """
+ Issues 'SETEVENTS BW' and parses a few events.
+ """
+
+ control_socket = test.runner.get_runner().get_tor_socket()
+ if not control_socket: self.skipTest("(no control socket)")
+ control_socket_file = control_socket.makefile()
+
+ stem.socket.send_message(control_socket_file, "SETEVENTS BW")
+ setevents_response = stem.socket.recv_message(control_socket_file)
+ self.assertEquals("OK", str(setevents_response))
+ self.assertEquals(["OK"], list(setevents_response))
+ self.assertEquals("250 OK\r\n", setevents_response.raw_content())
+ self.assertEquals([("250", " ", "OK")], setevents_response.content())
+
+ # Tor will emit a BW event once per second. Parsing two of them.
+
+ for _ in range(2):
+ bw_event = stem.socket.recv_message(control_socket_file)
+ self.assertTrue(re.match("BW [0-9]+ [0-9]+", str(bw_event)))
+ self.assertTrue(re.match("650 BW [0-9]+ [0-9]+\r\n", bw_event.raw_content()))
+ self.assertEquals(("650", " "), bw_event.content()[0][:2])
+
+ control_socket.close()
+ control_socket_file.close()
+
diff --git a/test/integ/types/__init__.py b/test/integ/types/__init__.py
deleted file mode 100644
index 7ab4c07..0000000
--- a/test/integ/types/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""
-Integration tests for stem.types.
-"""
-
-__all__ = ["control_message"]
-
diff --git a/test/integ/types/control_message.py b/test/integ/types/control_message.py
deleted file mode 100644
index 392cd25..0000000
--- a/test/integ/types/control_message.py
+++ /dev/null
@@ -1,210 +0,0 @@
-"""
-Integration tests for the stem.types.ControlMessage class.
-"""
-
-import re
-import socket
-import unittest
-
-import stem.types
-import test.runner
-
-class TestControlMessage(unittest.TestCase):
- """
- Exercises the 'stem.types.ControlMessage' class with an actual tor instance.
- """
-
- def test_unestablished_socket(self):
- """
- Checks message parsing when we have a valid but unauthenticated socket.
- """
-
- control_socket = test.runner.get_runner().get_tor_socket(False)
- if not control_socket: self.skipTest("(no control socket)")
- control_socket_file = control_socket.makefile()
-
- # If an unauthenticated connection gets a message besides AUTHENTICATE or
- # PROTOCOLINFO then tor will give an 'Authentication required.' message and
- # hang up.
-
- stem.types.write_message(control_socket_file, "GETINFO version")
-
- auth_required_response = stem.types.read_message(control_socket_file)
- self.assertEquals("Authentication required.", str(auth_required_response))
- self.assertEquals(["Authentication required."], list(auth_required_response))
- self.assertEquals("514 Authentication required.\r\n", auth_required_response.raw_content())
- self.assertEquals([("514", " ", "Authentication required.")], auth_required_response.content())
-
- # The socket's broken but doesn't realize it yet. Send another message and
- # it should fail with a closed exception. With a control port we won't get
- # an error until we read from the socket. However, with a control socket
- # the write will cause a SocketError.
-
- try:
- stem.types.write_message(control_socket_file, "GETINFO version")
- except: pass
-
- self.assertRaises(stem.types.SocketClosed, stem.types.read_message, control_socket_file)
-
- # Additional socket usage should fail, and pulling more responses will fail
- # with more closed exceptions.
-
- self.assertRaises(stem.types.SocketError, stem.types.write_message, control_socket_file, "GETINFO version")
- self.assertRaises(stem.types.SocketClosed, stem.types.read_message, control_socket_file)
- self.assertRaises(stem.types.SocketClosed, stem.types.read_message, control_socket_file)
- self.assertRaises(stem.types.SocketClosed, stem.types.read_message, control_socket_file)
-
- # The socket connection is already broken so calling close shouldn't have
- # an impact.
-
- control_socket.close()
- self.assertRaises(stem.types.SocketError, stem.types.write_message, control_socket_file, "GETINFO version")
- self.assertRaises(stem.types.SocketClosed, stem.types.read_message, control_socket_file)
-
- # Tries again with the file explicitely closed. In python 2.7 the close
- # call will raise...
- # error: [Errno 32] Broken pipe
-
- try: control_socket_file.close()
- except: pass
-
- self.assertRaises(stem.types.SocketError, stem.types.write_message, control_socket_file, "GETINFO version")
-
- # receives: stem.types.SocketClosed: socket file has been closed
- self.assertRaises(stem.types.SocketClosed, stem.types.read_message, control_socket_file)
-
- def test_invalid_command(self):
- """
- Parses the response for a command which doesn't exist.
- """
-
- control_socket = test.runner.get_runner().get_tor_socket()
- if not control_socket: self.skipTest("(no control socket)")
- control_socket_file = control_socket.makefile()
-
- stem.types.write_message(control_socket_file, "blarg")
- unrecognized_command_response = stem.types.read_message(control_socket_file)
- self.assertEquals('Unrecognized command "blarg"', str(unrecognized_command_response))
- self.assertEquals(['Unrecognized command "blarg"'], list(unrecognized_command_response))
- self.assertEquals('510 Unrecognized command "blarg"\r\n', unrecognized_command_response.raw_content())
- self.assertEquals([('510', ' ', 'Unrecognized command "blarg"')], unrecognized_command_response.content())
-
- control_socket.close()
- control_socket_file.close()
-
- def test_invalid_getinfo(self):
- """
- Parses the response for a GETINFO query which doesn't exist.
- """
-
- control_socket = test.runner.get_runner().get_tor_socket()
- if not control_socket: self.skipTest("(no control socket)")
- control_socket_file = control_socket.makefile()
-
- stem.types.write_message(control_socket_file, "GETINFO blarg")
- unrecognized_key_response = stem.types.read_message(control_socket_file)
- self.assertEquals('Unrecognized key "blarg"', str(unrecognized_key_response))
- self.assertEquals(['Unrecognized key "blarg"'], list(unrecognized_key_response))
- self.assertEquals('552 Unrecognized key "blarg"\r\n', unrecognized_key_response.raw_content())
- self.assertEquals([('552', ' ', 'Unrecognized key "blarg"')], unrecognized_key_response.content())
-
- control_socket.close()
- control_socket_file.close()
-
- def test_getinfo_config_file(self):
- """
- Parses the 'GETINFO config-file' response.
- """
-
- runner = test.runner.get_runner()
- torrc_dst = runner.get_torrc_path()
-
- control_socket = runner.get_tor_socket()
- if not control_socket: self.skipTest("(no control socket)")
- control_socket_file = control_socket.makefile()
-
- stem.types.write_message(control_socket_file, "GETINFO config-file")
- config_file_response = stem.types.read_message(control_socket_file)
- self.assertEquals("config-file=%s\nOK" % torrc_dst, str(config_file_response))
- self.assertEquals(["config-file=%s" % torrc_dst, "OK"], list(config_file_response))
- self.assertEquals("250-config-file=%s\r\n250 OK\r\n" % torrc_dst, config_file_response.raw_content())
- self.assertEquals([("250", "-", "config-file=%s" % torrc_dst), ("250", " ", "OK")], config_file_response.content())
-
- control_socket.close()
- control_socket_file.close()
-
- def test_getinfo_config_text(self):
- """
- Parses the 'GETINFO config-text' response.
- """
-
- if stem.process.get_tor_version() < stem.types.REQ_GETINFO_CONFIG_TEXT:
- self.skipTest("(requires %s)" % stem.types.REQ_GETINFO_CONFIG_TEXT)
-
- # We can't be certain of the order, and there may be extra config-text
- # entries as per...
- # https://trac.torproject.org/projects/tor/ticket/2362
- #
- # so we'll just check that the response is a superset of our config
-
- runner = test.runner.get_runner()
- torrc_contents = []
-
- for line in runner.get_torrc_contents().split("\n"):
- line = line.strip()
-
- if line and not line.startswith("#"):
- torrc_contents.append(line)
-
- control_socket = runner.get_tor_socket()
- if not control_socket: self.skipTest("(no control socket)")
- control_socket_file = control_socket.makefile()
-
- stem.types.write_message(control_socket_file, "GETINFO config-text")
- config_text_response = stem.types.read_message(control_socket_file)
-
- # the response should contain two entries, the first being a data response
- self.assertEqual(2, len(list(config_text_response)))
- self.assertEqual("OK", list(config_text_response)[1])
- self.assertEqual(("250", " ", "OK"), config_text_response.content()[1])
- self.assertTrue(config_text_response.raw_content().startswith("250+config-text=\r\n"))
- self.assertTrue(config_text_response.raw_content().endswith("\r\n.\r\n250 OK\r\n"))
- self.assertTrue(str(config_text_response).startswith("config-text=\n"))
- self.assertTrue(str(config_text_response).endswith("\nOK"))
-
- for torrc_entry in torrc_contents:
- self.assertTrue("\n%s\n" % torrc_entry in str(config_text_response))
- self.assertTrue(torrc_entry in list(config_text_response)[0])
- self.assertTrue("%s\r\n" % torrc_entry in config_text_response.raw_content())
- self.assertTrue("%s" % torrc_entry in config_text_response.content()[0][2])
-
- control_socket.close()
- control_socket_file.close()
-
- def test_bw_event(self):
- """
- Issues 'SETEVENTS BW' and parses a few events.
- """
-
- control_socket = test.runner.get_runner().get_tor_socket()
- if not control_socket: self.skipTest("(no control socket)")
- control_socket_file = control_socket.makefile()
-
- stem.types.write_message(control_socket_file, "SETEVENTS BW")
- setevents_response = stem.types.read_message(control_socket_file)
- self.assertEquals("OK", str(setevents_response))
- self.assertEquals(["OK"], list(setevents_response))
- self.assertEquals("250 OK\r\n", setevents_response.raw_content())
- self.assertEquals([("250", " ", "OK")], setevents_response.content())
-
- # Tor will emit a BW event once per second. Parsing two of them.
-
- for _ in range(2):
- bw_event = stem.types.read_message(control_socket_file)
- self.assertTrue(re.match("BW [0-9]+ [0-9]+", str(bw_event)))
- self.assertTrue(re.match("650 BW [0-9]+ [0-9]+\r\n", bw_event.raw_content()))
- self.assertEquals(("650", " "), bw_event.content()[0][:2])
-
- control_socket.close()
- control_socket_file.close()
-
diff --git a/test/runner.py b/test/runner.py
index bc8e8ec..89208e3 100644
--- a/test/runner.py
+++ b/test/runner.py
@@ -341,13 +341,13 @@ class Runner:
auth_cookie_contents = auth_cookie.read()
auth_cookie.close()
- stem.types.write_message(control_socket_file, "AUTHENTICATE %s" % binascii.b2a_hex(auth_cookie_contents))
+ stem.socket.send_message(control_socket_file, "AUTHENTICATE %s" % binascii.b2a_hex(auth_cookie_contents))
elif OPT_PASSWORD in conn_opts:
- stem.types.write_message(control_socket_file, "AUTHENTICATE \"%s\"" % CONTROL_PASSWORD)
+ stem.socket.send_message(control_socket_file, "AUTHENTICATE \"%s\"" % CONTROL_PASSWORD)
else:
- stem.types.write_message(control_socket_file, "AUTHENTICATE")
+ stem.socket.send_message(control_socket_file, "AUTHENTICATE")
- authenticate_response = stem.types.read_message(control_socket_file)
+ authenticate_response = stem.socket.recv_message(control_socket_file)
control_socket_file.close()
if str(authenticate_response) != "OK":
diff --git a/test/unit/connection/protocolinfo.py b/test/unit/connection/protocolinfo.py
index 3014cbb..b597981 100644
--- a/test/unit/connection/protocolinfo.py
+++ b/test/unit/connection/protocolinfo.py
@@ -5,6 +5,7 @@ Unit tests for the stem.connection.ProtocolInfoResponse class.
import unittest
import StringIO
import stem.connection
+import stem.socket
import stem.types
NO_AUTH = """250-PROTOCOLINFO 1
@@ -59,11 +60,11 @@ class TestProtocolInfoResponse(unittest.TestCase):
"""
# working case
- control_message = stem.types.read_message(StringIO.StringIO(NO_AUTH))
+ control_message = stem.socket.recv_message(StringIO.StringIO(NO_AUTH))
stem.connection.ProtocolInfoResponse.convert(control_message)
# now this should be a ProtocolInfoResponse (ControlMessage subclass)
- self.assertTrue(isinstance(control_message, stem.types.ControlMessage))
+ self.assertTrue(isinstance(control_message, stem.socket.ControlMessage))
self.assertTrue(isinstance(control_message, stem.connection.ProtocolInfoResponse))
# exercise some of the ControlMessage functionality
@@ -74,15 +75,15 @@ class TestProtocolInfoResponse(unittest.TestCase):
self.assertRaises(TypeError, stem.connection.ProtocolInfoResponse.convert, "hello world")
# attempt to convert a different message type
- bw_event_control_message = stem.types.read_message(StringIO.StringIO("650 BW 32326 2856\r\n"))
- self.assertRaises(stem.types.ProtocolError, stem.connection.ProtocolInfoResponse.convert, bw_event_control_message)
+ bw_event_control_message = stem.socket.recv_message(StringIO.StringIO("650 BW 32326 2856\r\n"))
+ 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 = stem.types.read_message(StringIO.StringIO(NO_AUTH))
+ control_message = stem.socket.recv_message(StringIO.StringIO(NO_AUTH))
stem.connection.ProtocolInfoResponse.convert(control_message)
self.assertEquals(1, control_message.protocol_version)
@@ -97,7 +98,7 @@ class TestProtocolInfoResponse(unittest.TestCase):
Checks a response with password authentication.
"""
- control_message = stem.types.read_message(StringIO.StringIO(PASSWORD_AUTH))
+ control_message = stem.socket.recv_message(StringIO.StringIO(PASSWORD_AUTH))
stem.connection.ProtocolInfoResponse.convert(control_message)
self.assertEquals((stem.connection.AuthMethod.PASSWORD, ), control_message.auth_methods)
@@ -107,7 +108,7 @@ class TestProtocolInfoResponse(unittest.TestCase):
characters.
"""
- control_message = stem.types.read_message(StringIO.StringIO(COOKIE_AUTH))
+ control_message = stem.socket.recv_message(StringIO.StringIO(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)
@@ -117,7 +118,7 @@ class TestProtocolInfoResponse(unittest.TestCase):
Checks a response with multiple authentication methods.
"""
- control_message = stem.types.read_message(StringIO.StringIO(MULTIPLE_AUTH))
+ control_message = stem.socket.recv_message(StringIO.StringIO(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)
@@ -127,7 +128,7 @@ class TestProtocolInfoResponse(unittest.TestCase):
Checks a response with an unrecognized authtentication method.
"""
- control_message = stem.types.read_message(StringIO.StringIO(UNKNOWN_AUTH))
+ control_message = stem.socket.recv_message(StringIO.StringIO(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)
@@ -138,7 +139,7 @@ class TestProtocolInfoResponse(unittest.TestCase):
information to be a valid response.
"""
- control_message = stem.types.read_message(StringIO.StringIO(MINIMUM_RESPONSE))
+ control_message = stem.socket.recv_message(StringIO.StringIO(MINIMUM_RESPONSE))
stem.connection.ProtocolInfoResponse.convert(control_message)
self.assertEquals(5, control_message.protocol_version)
@@ -167,7 +168,7 @@ class TestProtocolInfoResponse(unittest.TestCase):
stem.util.system.CALL_MOCKING = call_mocking
- control_message = stem.types.read_message(StringIO.StringIO(RELATIVE_COOKIE_PATH))
+ control_message = stem.socket.recv_message(StringIO.StringIO(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)
@@ -175,7 +176,7 @@ class TestProtocolInfoResponse(unittest.TestCase):
# leaving the path unexpanded)
stem.util.system.CALL_MOCKING = lambda cmd: None
- control_message = stem.types.read_message(StringIO.StringIO(RELATIVE_COOKIE_PATH))
+ control_message = stem.socket.recv_message(StringIO.StringIO(RELATIVE_COOKIE_PATH))
stem.connection.ProtocolInfoResponse.convert(control_message)
self.assertEquals("./tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path)
diff --git a/test/unit/socket/__init__.py b/test/unit/socket/__init__.py
new file mode 100644
index 0000000..a65ef67
--- /dev/null
+++ b/test/unit/socket/__init__.py
@@ -0,0 +1,6 @@
+"""
+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
new file mode 100644
index 0000000..ceb350f
--- /dev/null
+++ b/test/unit/socket/control_line.py
@@ -0,0 +1,163 @@
+"""
+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):
+ """
+ Tests methods of the stem.socket.ControlLine class.
+ """
+
+ 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.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.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.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())
+
+ 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))
+
+ # 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)
+
+ # 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())
+
+ 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
new file mode 100644
index 0000000..92d79c2
--- /dev/null
+++ b/test/unit/socket/control_message.py
@@ -0,0 +1,189 @@
+"""
+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):
+ """
+ Tests methods and functions related to 'stem.socket.ControlMessage'. This uses
+ StringIO to make 'files' to mock socket input.
+ """
+
+ 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).split("\n")))
+
+ # 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).split("\n")))
+
+ # 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.split("\n")[:-1]]
+
+ 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).split("\n")
+ 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/types/__init__.py b/test/unit/types/__init__.py
index d472371..95da593 100644
--- a/test/unit/types/__init__.py
+++ b/test/unit/types/__init__.py
@@ -2,5 +2,5 @@
Unit tests for stem.types.
"""
-__all__ = ["control_message", "control_line", "version"]
+__all__ = ["version"]
diff --git a/test/unit/types/control_line.py b/test/unit/types/control_line.py
deleted file mode 100644
index 0c324e5..0000000
--- a/test/unit/types/control_line.py
+++ /dev/null
@@ -1,163 +0,0 @@
-"""
-Unit tests for the stem.types.ControlLine class.
-"""
-
-import unittest
-import stem.types
-
-# 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):
- """
- Tests methods of the stem.types.ControlLine class.
- """
-
- def test_pop_examples(self):
- """
- Checks that the pop method's pydoc examples are correct.
- """
-
- line = stem.types.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.types.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.types.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.types.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.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.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.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())
-
- 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.types.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))
-
- # 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)
-
- # try popping this as a quoted mapping
- line = stem.types.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())
-
- 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.types.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.types.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.types.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.types.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.types.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/types/control_message.py b/test/unit/types/control_message.py
deleted file mode 100644
index af8fa00..0000000
--- a/test/unit/types/control_message.py
+++ /dev/null
@@ -1,189 +0,0 @@
-"""
-Unit tests for the stem.types.ControlMessage parsing and class.
-"""
-
-import socket
-import StringIO
-import unittest
-import stem.types
-
-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):
- """
- Tests methods and functions related to 'stem.types.ControlMessage'. This uses
- StringIO to make 'files' to mock socket input.
- """
-
- 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).split("\n")))
-
- # 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).split("\n")))
-
- # 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.split("\n")[:-1]]
-
- 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.types.ProtocolError, stem.types.read_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.types.ProtocolError, stem.types.read_message, StringIO.StringIO(removal_test_input))
- self.assertRaises(stem.types.ProtocolError, stem.types.read_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.types.SocketClosed, stem.types.read_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.types.ControlMessage for the given input
- """
-
- message = stem.types.read_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).split("\n")
- 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

26 Nov '11
commit fd90d6c50011b8764b1c82c448fe39eab76c9c20
Author: Damian Johnson <atagar(a)torproject.org>
Date: Fri Nov 25 22:55:57 2011 -0800
Using space or newline div when logging
When logging a multi-line message using a newline divider with the "Sending:"
or "Receiving:" prefix, otherwise using a space (minor bug had the space always
included previously).
---
stem/socket.py | 22 ++++++++--------------
1 files changed, 8 insertions(+), 14 deletions(-)
diff --git a/stem/socket.py b/stem/socket.py
index 451b535..8add15f 100644
--- a/stem/socket.py
+++ b/stem/socket.py
@@ -517,14 +517,12 @@ def send_message(control_file, message, raw = False):
if not raw: message = send_formatting(message)
+ # uses a newline divider if this is a multi-line message (more readable)
+ log_message = message.replace("\r\n", "\n").rstrip()
+ div = "\n" if "\n" in log_message else " "
+ LOGGER.debug("Sending:" + div + log_message)
+
try:
- log_message = message.replace("\r\n", "\n").rstrip()
-
- # starts with a newline if this is a multi-line message (more readable)
- if "\n" in log_message: log_message = "\n" + log_message
-
- LOGGER.debug("Sending: " + log_message)
-
control_file.write(message)
control_file.flush()
except socket.error, exc:
@@ -603,14 +601,10 @@ def recv_message(control_file):
# end of the message, return the message
parsed_content.append((status_code, divider, content))
- # replacing the \r\n newline endings and the ending newline since it
- # leads to more readable log messages
+ # uses a newline divider if this is a multi-line message (more readable)
log_message = raw_content.replace("\r\n", "\n").rstrip()
-
- # starts with a newline if this is a multi-line message (more readable)
- if "\n" in log_message: log_message = "\n" + log_message
-
- LOGGER.debug("Received: " + log_message)
+ div = "\n" if "\n" in log_message else " "
+ LOGGER.debug("Received:" + div + log_message)
return ControlMessage(parsed_content, raw_content)
elif divider == "+":
1
0

26 Nov '11
commit 8f8257d4ddcf6830b2413f53890a1fab4414ab45
Author: Damian Johnson <atagar(a)torproject.org>
Date: Fri Nov 25 22:42:35 2011 -0800
Only respecting open default when undefined
When no target is defined we should have a test.runner.TorConnection.OPEN
default for integraiton tests. However, if we have an alternative connection
target then this should be overwritten.
---
run_tests.py | 22 ++++++++++++----------
1 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/run_tests.py b/run_tests.py
index 8fa7107..800cab2 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -214,19 +214,21 @@ if __name__ == '__main__':
else:
# mapping of config type to (enum, default) tuple
conn_type_mappings = {
- "none": (test.runner.TorConnection.NONE, False),
- "open": (test.runner.TorConnection.OPEN, True),
- "password": (test.runner.TorConnection.PASSWORD, False),
- "cookie": (test.runner.TorConnection.COOKIE, False),
- "multiple": (test.runner.TorConnection.MULTIPLE, False),
- "socket": (test.runner.TorConnection.SOCKET, False),
+ "none": test.runner.TorConnection.NONE,
+ "open": test.runner.TorConnection.OPEN,
+ "password": test.runner.TorConnection.PASSWORD,
+ "cookie": test.runner.TorConnection.COOKIE,
+ "multiple": test.runner.TorConnection.MULTIPLE,
+ "socket": test.runner.TorConnection.SOCKET,
}
for type_key in conn_type_mappings:
- default = conn_type_mappings[type_key][1]
-
- if test_config.get("test.integ.target.connection.%s" % type_key, default):
- connection_types.append(conn_type_mappings[type_key][0])
+ if test_config.get("test.integ.target.connection.%s" % type_key, False):
+ connection_types.append(conn_type_mappings[type_key])
+
+ # TorConnection.OPEN is the default if we don't have any defined
+ if not connection_types:
+ connection_types = [test.runner.TorConnection.OPEN]
for connection_type in connection_types:
try:
1
0
commit 5b505ec579ab7edcaf55d1e35a2ccd3b9aee6672
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Nov 26 10:10:21 2011 -0800
Fixing relative cookie expansion test
A couple protocolinfo tests filtered system calls so that pid lookups by
process name would fall and we'd fall back on looking it up by the control port
or socket file (to exercise alternative code paths). However, I'd forgotten
that this would also filter out the get_cwd lookup calls, causing those tests
to fail.
The relative cookie expansion by socket file wasn't being exercised at all
because I didn't have a integ test configuration where we had both a control
socket and authentication cookie. I've added this test now and fixed this issue
with the socket test too.
---
run_tests.py | 4 ++-
test/integ/connection/protocolinfo.py | 34 +++++++++++++++++++++++---------
test/runner.py | 3 +-
test/testrc.sample | 1 +
4 files changed, 30 insertions(+), 12 deletions(-)
diff --git a/run_tests.py b/run_tests.py
index 800cab2..8dd141e 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -47,7 +47,7 @@ INTEG_TESTS = (("stem.socket.ControlMessage", test.integ.socket.control_message.
)
# Integration tests above the basic suite.
-TARGETS = stem.util.enum.Enum(*[(v, v) for v in ("ONLINE", "RELATIVE", "CONN_NONE", "CONN_OPEN", "CONN_PASSWORD", "CONN_COOKIE", "CONN_MULTIPLE", "CONN_SOCKET", "CONN_ALL")])
+TARGETS = stem.util.enum.Enum(*[(v, v) for v in ("ONLINE", "RELATIVE", "CONN_NONE", "CONN_OPEN", "CONN_PASSWORD", "CONN_COOKIE", "CONN_MULTIPLE", "CONN_SOCKET", "CONN_SCOOKIE", "CONN_ALL")])
TARGET_ATTR = {
TARGETS.ONLINE: ("test.integ.target.online", "Includes tests that require network activity."),
@@ -58,6 +58,7 @@ TARGET_ATTR = {
TARGETS.CONN_COOKIE: ("test.integ.target.connection.cookie", "Configuration with an authentication cookie."),
TARGETS.CONN_MULTIPLE: ("test.integ.target.connection.multiple", "Configuration with both password and cookie authentication."),
TARGETS.CONN_SOCKET: ("test.integ.target.connection.socket", "Configuration with a control socket."),
+ TARGETS.CONN_SCOOKIE: ("test.integ.target.connection.scookie", "Configuration with a control socket and authentication cookie."),
TARGETS.CONN_ALL: ("test.integ.target.connection.all", "Runs integration tests for all connection configurations."),
}
@@ -220,6 +221,7 @@ if __name__ == '__main__':
"cookie": test.runner.TorConnection.COOKIE,
"multiple": test.runner.TorConnection.MULTIPLE,
"socket": test.runner.TorConnection.SOCKET,
+ "scookie": test.runner.TorConnection.SCOOKIE,
}
for type_key in conn_type_mappings:
diff --git a/test/integ/connection/protocolinfo.py b/test/integ/connection/protocolinfo.py
index 71d2d6c..8c23329 100644
--- a/test/integ/connection/protocolinfo.py
+++ b/test/integ/connection/protocolinfo.py
@@ -16,6 +16,10 @@ class TestProtocolInfo(unittest.TestCase):
integ target to exercise the widest range of use cases.
"""
+ def tearDown(self):
+ # resets call mocking back to being disabled
+ stem.util.system.CALL_MOCKING = None
+
def test_parsing(self):
"""
Makes a PROTOCOLINFO query and processes the response for our control
@@ -53,15 +57,17 @@ class TestProtocolInfo(unittest.TestCase):
# If we have both the 'RELATIVE' target and a cookie then test_parsing
# should exercise cookie expansion using a pid lookup by process name.
# Disabling those lookups so we exercise the lookup by port/socket file
- # too.
+ # too. Gotta remember the get_cwd functions too.
- port_lookup_prefixes = (
+ 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_PID_BY_PORT_LSOF,
+ stem.util.system.GET_CWD_PWDX % "",
+ "lsof -a -p ")
def port_lookup_filter(command):
- for prefix in port_lookup_prefixes:
+ for prefix in cwd_by_port_lookup_prefixes:
if command.startswith(prefix): return True
return False
@@ -76,16 +82,24 @@ class TestProtocolInfo(unittest.TestCase):
else:
# we don't have a control port
self.assertRaises(stem.socket.SocketError, stem.connection.get_protocolinfo_by_port, "127.0.0.1", test.runner.CONTROL_PORT)
-
- stem.util.system.CALL_MOCKING = None
def test_get_protocolinfo_by_socket(self):
"""
Exercises the stem.connection.get_protocolinfo_by_socket function.
"""
- socket_file_prefix = stem.util.system.GET_PID_BY_FILE_LSOF % ""
- stem.util.system.CALL_MOCKING = lambda cmd: cmd.startswith(socket_file_prefix)
+ cwd_by_socket_lookup_prefixes = (
+ stem.util.system.GET_PID_BY_FILE_LSOF % "",
+ stem.util.system.GET_CWD_PWDX % "",
+ "lsof -a -p ")
+
+ def socket_lookup_filter(command):
+ for prefix in cwd_by_socket_lookup_prefixes:
+ if command.startswith(prefix): return True
+
+ return False
+
+ stem.util.system.CALL_MOCKING = socket_lookup_filter
connection_type = test.runner.get_runner().get_connection_type()
if test.runner.OPT_SOCKET in test.runner.CONNECTION_OPTS[connection_type]:
@@ -95,8 +109,6 @@ class TestProtocolInfo(unittest.TestCase):
else:
# we don't have a control socket
self.assertRaises(stem.socket.SocketError, stem.connection.get_protocolinfo_by_socket, test.runner.CONTROL_SOCKET_PATH)
-
- stem.util.system.CALL_MOCKING = None
def assert_protocolinfo_attr(self, protocolinfo_response, connection_type):
"""
@@ -117,6 +129,8 @@ class TestProtocolInfo(unittest.TestCase):
auth_methods = (stem.connection.AuthMethod.COOKIE, stem.connection.AuthMethod.PASSWORD)
elif connection_type == test.runner.TorConnection.SOCKET:
auth_methods = (stem.connection.AuthMethod.NONE,)
+ elif connection_type == test.runner.TorConnection.SCOOKIE:
+ auth_methods = (stem.connection.AuthMethod.COOKIE,)
else:
self.fail("Unrecognized connection type: %s" % connection_type)
diff --git a/test/runner.py b/test/runner.py
index 89208e3..52e9d25 100644
--- a/test/runner.py
+++ b/test/runner.py
@@ -42,7 +42,7 @@ DEFAULT_CONFIG = {
# Methods for connecting to tor. General integration tests only run with the
# DEFAULT_TOR_CONNECTION, but expanded integ tests will run with all of them.
-TorConnection = stem.util.enum.Enum("NONE", "OPEN", "PASSWORD", "COOKIE", "MULTIPLE", "SOCKET")
+TorConnection = stem.util.enum.Enum("NONE", "OPEN", "PASSWORD", "COOKIE", "MULTIPLE", "SOCKET", "SCOOKIE")
DEFAULT_TOR_CONNECTION = TorConnection.OPEN
STATUS_ATTR = (term.Color.BLUE, term.Attr.BOLD)
@@ -80,6 +80,7 @@ CONNECTION_OPTS = {
TorConnection.COOKIE: [OPT_PORT, OPT_COOKIE],
TorConnection.MULTIPLE: [OPT_PORT, OPT_PASSWORD, OPT_COOKIE],
TorConnection.SOCKET: [OPT_SOCKET],
+ TorConnection.SCOOKIE: [OPT_SOCKET, OPT_COOKIE],
}
def get_runner():
diff --git a/test/testrc.sample b/test/testrc.sample
index 28874e9..f0b917d 100644
--- a/test/testrc.sample
+++ b/test/testrc.sample
@@ -40,5 +40,6 @@ test.integ.target.connection.password false
test.integ.target.connection.cookie false
test.integ.target.connection.muiltipe false
test.integ.target.connection.socket false
+test.integ.target.connection.scookie false
test.integ.target.connection.all false
1
0