[tor-commits] [stem/master] Moving control message handling into stem.socket

atagar at torproject.org atagar at torproject.org
Sat Nov 26 18:28:32 UTC 2011


commit 9a06ff17de582e834e2791c096bd9a51834a6d66
Author: Damian Johnson <atagar at 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
-





More information about the tor-commits mailing list