Esempio n. 1
0
def recv_message(control_file):
    """
  Pulls from a control socket until we either have a complete message or
  encounter a problem.

  :param file control_file: file derived from the control socket (see the
    socket's makefile() method for more information)

  :returns: :class:`~stem.response.ControlMessage` read from the socket

  :raises:
    * :class:`stem.ProtocolError` the content from the socket is malformed
    * :class:`stem.SocketClosed` if the socket closes before we receive
      a complete message
  """

    parsed_content, raw_content = [], []
    logging_prefix = 'Error while receiving a control message (%s): '

    while True:
        try:
            # From a real socket readline() would always provide bytes, but during
            # tests we might be given a StringIO in which case it's unicode under
            # python 3.x.

            line = stem.util.str_tools._to_bytes(control_file.readline())
        except AttributeError:
            # if the control_file has been closed then we will receive:
            # AttributeError: 'NoneType' object has no attribute 'recv'

            prefix = logging_prefix % 'SocketClosed'
            log.info(prefix + 'socket file has been closed')
            raise stem.SocketClosed('socket file has been closed')
        except (socket.error, ValueError) as exc:
            # When disconnected we get...
            #
            # Python 2:
            #   socket.error: [Errno 107] Transport endpoint is not connected
            #
            # Python 3:
            #   ValueError: I/O operation on closed file.

            prefix = logging_prefix % 'SocketClosed'
            log.info(prefix + 'received exception "%s"' % exc)
            raise stem.SocketClosed(exc)

        raw_content.append(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

            prefix = logging_prefix % 'SocketClosed'
            log.info(prefix + 'empty socket content')
            raise stem.SocketClosed('Received empty socket content.')
        elif len(line) < 4:
            prefix = logging_prefix % 'ProtocolError'
            log.info(prefix + 'line too short, "%s"' % log.escape(line))
            raise stem.ProtocolError('Badly formatted reply line: too short')
        elif not re.match(b'^[a-zA-Z0-9]{3}[-+ ]', line):
            prefix = logging_prefix % 'ProtocolError'
            log.info(prefix +
                     'malformed status code/divider, "%s"' % log.escape(line))
            raise stem.ProtocolError(
                'Badly formatted reply line: beginning is malformed')
        elif not line.endswith(b'\r\n'):
            prefix = logging_prefix % 'ProtocolError'
            log.info(prefix + 'no CRLF linebreak, "%s"' % log.escape(line))
            raise stem.ProtocolError('All lines should end with CRLF')

        line = line[:-2]  # strips off the CRLF
        status_code, divider, content = line[:3], line[3:4], line[4:]
        content_lines = [content]

        if stem.prereq.is_python_3():
            status_code = stem.util.str_tools._to_unicode(status_code)
            divider = stem.util.str_tools._to_unicode(divider)

        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))

            raw_content_str = b''.join(raw_content)
            log_message = stem.util.str_tools._to_unicode(
                raw_content_str.replace(b'\r\n', b'\n').rstrip())
            log_message_lines = log_message.split('\n')

            if TRUNCATE_LOGS and len(log_message_lines) > TRUNCATE_LOGS:
                log_message = '\n'.join(log_message_lines[:TRUNCATE_LOGS] + [
                    '... %i more lines...' %
                    (len(log_message_lines) - TRUNCATE_LOGS)
                ])

            if len(log_message_lines) > 2:
                log.trace('Received from tor:\n%s' % log_message)
            else:
                log.trace('Received from tor: %s' %
                          log_message.replace('\n', '\\n'))

            return stem.response.ControlMessage(parsed_content,
                                                raw_content_str)
        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 = stem.util.str_tools._to_bytes(
                        control_file.readline())
                except socket.error as exc:
                    prefix = logging_prefix % 'SocketClosed'
                    log.info(
                        prefix +
                        'received an exception while mid-way through a data reply (exception: "%s", read content: "%s")'
                        % (exc, log.escape(b''.join(raw_content))))
                    raise stem.SocketClosed(exc)

                raw_content.append(line)

                if not line.endswith(b'\r\n'):
                    prefix = logging_prefix % 'ProtocolError'
                    log.info(
                        prefix +
                        'CRLF linebreaks missing from a data reply, "%s"' %
                        log.escape(b''.join(raw_content)))
                    raise stem.ProtocolError('All lines should end with CRLF')
                elif line == b'.\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(b'..'):
                    line = line[1:]

                content_lines.append(line)

            # joins the content using a newline rather than CRLF separator (more
            # conventional for multi-line string content outside the windows world)

            parsed_content.append(
                (status_code, divider, b'\n'.join(content_lines)))
        else:
            # this should never be reached due to the prefix regex, but might as well
            # be safe...
            prefix = logging_prefix % 'ProtocolError'
            log.warn(prefix +
                     "\"%s\" isn't a recognized divider type" % divider)
            raise stem.ProtocolError(
                "Unrecognized divider type '%s': %s" %
                (divider, stem.util.str_tools._to_unicode(line)))
Esempio n. 2
0
def recv_message(control_file):
  """
  Pulls from a control socket until we either have a complete message or
  encounter a problem.

  :param file control_file: file derived from the control socket (see the
    socket's makefile() method for more information)

  :returns: :class:`~stem.response.ControlMessage` read from the socket

  :raises:
    * :class:`stem.ProtocolError` the content from the socket is malformed
    * :class:`stem.SocketClosed` if the socket closes before we receive
      a complete message
  """

  parsed_content, raw_content = [], ""
  logging_prefix = "Error while receiving a control message (%s): "

  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'

      prefix = logging_prefix % "SocketClosed"
      log.info(prefix + "socket file has been closed")
      raise stem.SocketClosed("socket file has been closed")
    except socket.error, exc:
      # when disconnected we get...
      # socket.error: [Errno 107] Transport endpoint is not connected

      prefix = logging_prefix % "SocketClosed"
      log.info(prefix + "received exception \"%s\"" % exc)
      raise stem.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

      prefix = logging_prefix % "SocketClosed"
      log.info(prefix + "empty socket content")
      raise stem.SocketClosed("Received empty socket content.")
    elif len(line) < 4:
      prefix = logging_prefix % "ProtocolError"
      log.info(prefix + "line too short, \"%s\"" % log.escape(line))
      raise stem.ProtocolError("Badly formatted reply line: too short")
    elif not re.match(r'^[a-zA-Z0-9]{3}[-+ ]', line):
      prefix = logging_prefix % "ProtocolError"
      log.info(prefix + "malformed status code/divider, \"%s\"" % log.escape(line))
      raise stem.ProtocolError("Badly formatted reply line: beginning is malformed")
    elif not line.endswith("\r\n"):
      prefix = logging_prefix % "ProtocolError"
      log.info(prefix + "no CRLF linebreak, \"%s\"" % log.escape(line))
      raise stem.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))

      log_message = raw_content.replace("\r\n", "\n").rstrip()
      log.trace("Received from tor:\n" + log_message)

      return stem.response.ControlMessage(parsed_content, raw_content)
    elif divider == "+":
      # data entry, all of the following lines belong to the content until we
      # get a line with just a period

      while True:
        try:
          line = control_file.readline()
        except socket.error, exc:
          prefix = logging_prefix % "SocketClosed"
          log.info(prefix + "received an exception while mid-way through a data reply (exception: \"%s\", read content: \"%s\")" % (exc, log.escape(raw_content)))
          raise stem.SocketClosed(exc)

        raw_content += line

        if not line.endswith("\r\n"):
          prefix = logging_prefix % "ProtocolError"
          log.info(prefix + "CRLF linebreaks missing from a data reply, \"%s\"" % log.escape(raw_content))
          raise stem.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))
Esempio n. 3
0
def recv_message(control_file, arrived_at = None):
  """
  Pulls from a control socket until we either have a complete message or
  encounter a problem.

  :param file control_file: file derived from the control socket (see the
    socket's makefile() method for more information)

  :returns: :class:`~stem.response.ControlMessage` read from the socket

  :raises:
    * :class:`stem.ProtocolError` the content from the socket is malformed
    * :class:`stem.SocketClosed` if the socket closes before we receive
      a complete message
  """

  parsed_content, raw_content, first_line = None, None, True

  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'

      log.info(ERROR_MSG % ('SocketClosed', 'socket file has been closed'))
      raise stem.SocketClosed('socket file has been closed')
    except (OSError, ValueError) as exc:
      # when disconnected this errors with...
      #
      #   * ValueError: I/O operation on closed file
      #   * OSError: [Errno 107] Transport endpoint is not connected
      #   * OSError: [Errno 9] Bad file descriptor

      log.info(ERROR_MSG % ('SocketClosed', 'received exception "%s"' % exc))
      raise stem.SocketClosed(exc)

    # Parses the tor control lines. These are of the form...
    # <status code><divider><content>\r\n

    if not line:
      # if the socket is disconnected then the readline() method will provide
      # empty content

      log.info(ERROR_MSG % ('SocketClosed', 'empty socket content'))
      raise stem.SocketClosed('Received empty socket content.')
    elif not MESSAGE_PREFIX.match(line):
      log.info(ERROR_MSG % ('ProtocolError', 'malformed status code/divider, "%s"' % log.escape(line)))
      raise stem.ProtocolError('Badly formatted reply line: beginning is malformed')
    elif not line.endswith(b'\r\n'):
      log.info(ERROR_MSG % ('ProtocolError', 'no CRLF linebreak, "%s"' % log.escape(line)))
      raise stem.ProtocolError('All lines should end with CRLF')

    status_code, divider, content = line[:3], line[3:4], line[4:-2]  # strip CRLF off content

    status_code = stem.util.str_tools._to_unicode(status_code)
    divider = stem.util.str_tools._to_unicode(divider)

    # Most controller responses are single lines, in which case we don't need
    # so much overhead.

    if first_line:
      if divider == ' ':
        _log_trace(line)
        return stem.response.ControlMessage([(status_code, divider, content)], line, arrived_at = arrived_at)
      else:
        parsed_content, raw_content, first_line = [], bytearray(), False

    raw_content += line

    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))
      _log_trace(bytes(raw_content))
      return stem.response.ControlMessage(parsed_content, bytes(raw_content), arrived_at = arrived_at)
    elif divider == '+':
      # data entry, all of the following lines belong to the content until we
      # get a line with just a period

      content_block = bytearray(content)

      while True:
        try:
          line = control_file.readline()
          raw_content += line
        except socket.error as exc:
          log.info(ERROR_MSG % ('SocketClosed', 'received an exception while mid-way through a data reply (exception: "%s", read content: "%s")' % (exc, log.escape(bytes(raw_content)))))
          raise stem.SocketClosed(exc)

        if not line.endswith(b'\r\n'):
          log.info(ERROR_MSG % ('ProtocolError', 'CRLF linebreaks missing from a data reply, "%s"' % log.escape(bytes(raw_content))))
          raise stem.ProtocolError('All lines should end with CRLF')
        elif line == b'.\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(b'..'):
          line = line[1:]

        content_block += b'\n' + line

      # joins the content using a newline rather than CRLF separator (more
      # conventional for multi-line string content outside the windows world)

      parsed_content.append((status_code, divider, bytes(content_block)))
    else:
      # this should never be reached due to the prefix regex, but might as well
      # be safe...

      log.warn(ERROR_MSG % ('ProtocolError', "\"%s\" isn't a recognized divider type" % divider))
      raise stem.ProtocolError("Unrecognized divider type '%s': %s" % (divider, stem.util.str_tools._to_unicode(line)))
Esempio n. 4
0
def recv_message(control_file):
    """
  Pulls from a control socket until we either have a complete message or
  encounter a problem.

  :param file control_file: file derived from the control socket (see the
    socket's makefile() method for more information)

  :returns: :class:`~stem.response.ControlMessage` read from the socket

  :raises:
    * :class:`stem.ProtocolError` the content from the socket is malformed
    * :class:`stem.SocketClosed` if the socket closes before we receive
      a complete message
  """

    parsed_content, raw_content = [], ""
    logging_prefix = "Error while receiving a control message (%s): "

    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'

            prefix = logging_prefix % "SocketClosed"
            log.info(prefix + "socket file has been closed")
            raise stem.SocketClosed("socket file has been closed")
        except socket.error, exc:
            # when disconnected we get...
            # socket.error: [Errno 107] Transport endpoint is not connected

            prefix = logging_prefix % "SocketClosed"
            log.info(prefix + "received exception \"%s\"" % exc)
            raise stem.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

            prefix = logging_prefix % "SocketClosed"
            log.info(prefix + "empty socket content")
            raise stem.SocketClosed("Received empty socket content.")
        elif len(line) < 4:
            prefix = logging_prefix % "ProtocolError"
            log.info(prefix + "line too short, \"%s\"" % log.escape(line))
            raise stem.ProtocolError("Badly formatted reply line: too short")
        elif not re.match(r'^[a-zA-Z0-9]{3}[-+ ]', line):
            prefix = logging_prefix % "ProtocolError"
            log.info(prefix + "malformed status code/divider, \"%s\"" %
                     log.escape(line))
            raise stem.ProtocolError(
                "Badly formatted reply line: beginning is malformed")
        elif not line.endswith("\r\n"):
            prefix = logging_prefix % "ProtocolError"
            log.info(prefix + "no CRLF linebreak, \"%s\"" % log.escape(line))
            raise stem.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))

            log_message = raw_content.replace("\r\n", "\n").rstrip()
            log.trace("Received from tor:\n" + log_message)

            return stem.response.ControlMessage(parsed_content, raw_content)
        elif divider == "+":
            # data entry, all of the following lines belong to the content until we
            # get a line with just a period

            while True:
                try:
                    line = control_file.readline()
                except socket.error, exc:
                    prefix = logging_prefix % "SocketClosed"
                    log.info(
                        prefix +
                        "received an exception while mid-way through a data reply (exception: \"%s\", read content: \"%s\")"
                        % (exc, log.escape(raw_content)))
                    raise stem.SocketClosed(exc)

                raw_content += line

                if not line.endswith("\r\n"):
                    prefix = logging_prefix % "ProtocolError"
                    log.info(
                        prefix +
                        "CRLF linebreaks missing from a data reply, \"%s\"" %
                        log.escape(raw_content))
                    raise stem.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))
Esempio n. 5
0
def recv_message(control_file):
  """
  Pulls from a control socket until we either have a complete message or
  encounter a problem.

  :param file control_file: file derived from the control socket (see the
    socket's makefile() method for more information)

  :returns: :class:`~stem.response.ControlMessage` read from the socket

  :raises:
    * :class:`stem.ProtocolError` the content from the socket is malformed
    * :class:`stem.SocketClosed` if the socket closes before we receive
      a complete message
  """

  parsed_content, raw_content = [], []
  logging_prefix = 'Error while receiving a control message (%s): '

  while True:
    try:
      # From a real socket readline() would always provide bytes, but during
      # tests we might be given a StringIO in which case it's unicode under
      # python 3.x.

      line = stem.util.str_tools._to_bytes(control_file.readline())
    except AttributeError:
      # if the control_file has been closed then we will receive:
      # AttributeError: 'NoneType' object has no attribute 'recv'

      prefix = logging_prefix % 'SocketClosed'
      log.info(prefix + 'socket file has been closed')
      raise stem.SocketClosed('socket file has been closed')
    except (socket.error, ValueError) as exc:
      # When disconnected we get...
      #
      # Python 2:
      #   socket.error: [Errno 107] Transport endpoint is not connected
      #
      # Python 3:
      #   ValueError: I/O operation on closed file.

      prefix = logging_prefix % 'SocketClosed'
      log.info(prefix + 'received exception "%s"' % exc)
      raise stem.SocketClosed(exc)

    raw_content.append(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

      prefix = logging_prefix % 'SocketClosed'
      log.info(prefix + 'empty socket content')
      raise stem.SocketClosed('Received empty socket content.')
    elif len(line) < 4:
      prefix = logging_prefix % 'ProtocolError'
      log.info(prefix + 'line too short, "%s"' % log.escape(line))
      raise stem.ProtocolError('Badly formatted reply line: too short')
    elif not re.match(b'^[a-zA-Z0-9]{3}[-+ ]', line):
      prefix = logging_prefix % 'ProtocolError'
      log.info(prefix + 'malformed status code/divider, "%s"' % log.escape(line))
      raise stem.ProtocolError('Badly formatted reply line: beginning is malformed')
    elif not line.endswith(b'\r\n'):
      prefix = logging_prefix % 'ProtocolError'
      log.info(prefix + 'no CRLF linebreak, "%s"' % log.escape(line))
      raise stem.ProtocolError('All lines should end with CRLF')

    line = line[:-2]  # strips off the CRLF
    status_code, divider, content = line[:3], line[3:4], line[4:]
    content_lines = [content]

    if stem.prereq.is_python_3():
      status_code = stem.util.str_tools._to_unicode(status_code)
      divider = stem.util.str_tools._to_unicode(divider)

    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))

      raw_content_str = b''.join(raw_content)
      log_message = raw_content_str.replace(b'\r\n', b'\n').rstrip()
      log.trace('Received from tor:\n' + stem.util.str_tools._to_unicode(log_message))

      return stem.response.ControlMessage(parsed_content, raw_content_str)
    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 = stem.util.str_tools._to_bytes(control_file.readline())
        except socket.error as exc:
          prefix = logging_prefix % 'SocketClosed'
          log.info(prefix + 'received an exception while mid-way through a data reply (exception: "%s", read content: "%s")' % (exc, log.escape(b''.join(raw_content))))
          raise stem.SocketClosed(exc)

        raw_content.append(line)

        if not line.endswith(b'\r\n'):
          prefix = logging_prefix % 'ProtocolError'
          log.info(prefix + 'CRLF linebreaks missing from a data reply, "%s"' % log.escape(b''.join(raw_content)))
          raise stem.ProtocolError('All lines should end with CRLF')
        elif line == b'.\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(b'..'):
          line = line[1:]

        content_lines.append(line)

      # joins the content using a newline rather than CRLF separator (more
      # conventional for multi-line string content outside the windows world)

      parsed_content.append((status_code, divider, b'\n'.join(content_lines)))
    else:
      # this should never be reached due to the prefix regex, but might as well
      # be safe...
      prefix = logging_prefix % 'ProtocolError'
      log.warn(prefix + "\"%s\" isn't a recognized divider type" % divider)
      raise stem.ProtocolError("Unrecognized divider type '%s': %s" % (divider, stem.util.str_tools._to_unicode(line)))
Esempio n. 6
0
def recv_message(control_file):
    """
  Pulls from a control socket until we either have a complete message or
  encounter a problem.

  :param file control_file: file derived from the control socket (see the
    socket's makefile() method for more information)

  :returns: :class:`~stem.response.ControlMessage` read from the socket

  :raises:
    * :class:`stem.ProtocolError` the content from the socket is malformed
    * :class:`stem.SocketClosed` if the socket closes before we receive
      a complete message
  """

    parsed_content, raw_content = [], b""
    logging_prefix = "Error while receiving a control message (%s): "

    while True:
        try:
            # From a real socket readline() would always provide bytes, but during
            # tests we might be given a StringIO in which case it's unicode under
            # python 3.x.

            line = stem.util.str_tools._to_bytes(control_file.readline())
        except AttributeError:
            # if the control_file has been closed then we will receive:
            # AttributeError: 'NoneType' object has no attribute 'recv'

            prefix = logging_prefix % "SocketClosed"
            log.info(prefix + "socket file has been closed")
            raise stem.SocketClosed("socket file has been closed")
        except (socket.error, ValueError) as exc:
            # When disconnected we get...
            #
            # Python 2:
            #   socket.error: [Errno 107] Transport endpoint is not connected
            #
            # Python 3:
            #   ValueError: I/O operation on closed file.

            prefix = logging_prefix % "SocketClosed"
            log.info(prefix + "received exception \"%s\"" % exc)
            raise stem.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

            prefix = logging_prefix % "SocketClosed"
            log.info(prefix + "empty socket content")
            raise stem.SocketClosed("Received empty socket content.")
        elif len(line) < 4:
            prefix = logging_prefix % "ProtocolError"
            log.info(prefix + "line too short, \"%s\"" % log.escape(line))
            raise stem.ProtocolError("Badly formatted reply line: too short")
        elif not re.match(b'^[a-zA-Z0-9]{3}[-+ ]', line):
            prefix = logging_prefix % "ProtocolError"
            log.info(prefix + "malformed status code/divider, \"%s\"" %
                     log.escape(line))
            raise stem.ProtocolError(
                "Badly formatted reply line: beginning is malformed")
        elif not line.endswith(b"\r\n"):
            prefix = logging_prefix % "ProtocolError"
            log.info(prefix + "no CRLF linebreak, \"%s\"" % log.escape(line))
            raise stem.ProtocolError("All lines should end with CRLF")

        line = line[:-2]  # strips off the CRLF
        status_code, divider, content = line[:3], line[3:4], line[4:]

        if stem.prereq.is_python_3():
            status_code = stem.util.str_tools._to_unicode(status_code)
            divider = stem.util.str_tools._to_unicode(divider)

        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))

            log_message = raw_content.replace(b"\r\n", b"\n").rstrip()
            log.trace("Received from tor:\n" +
                      stem.util.str_tools._to_unicode(log_message))

            return stem.response.ControlMessage(parsed_content, raw_content)
        elif divider == "+":
            # data entry, all of the following lines belong to the content until we
            # get a line with just a period

            while True:
                try:
                    line = stem.util.str_tools._to_bytes(
                        control_file.readline())
                except socket.error as exc:
                    prefix = logging_prefix % "SocketClosed"
                    log.info(
                        prefix +
                        "received an exception while mid-way through a data reply (exception: \"%s\", read content: \"%s\")"
                        % (exc, log.escape(raw_content)))
                    raise stem.SocketClosed(exc)

                raw_content += line

                if not line.endswith(b"\r\n"):
                    prefix = logging_prefix % "ProtocolError"
                    log.info(
                        prefix +
                        "CRLF linebreaks missing from a data reply, \"%s\"" %
                        log.escape(raw_content))
                    raise stem.ProtocolError("All lines should end with CRLF")
                elif line == b".\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(b".."):
                    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 += b"\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...
            prefix = logging_prefix % "ProtocolError"
            log.warn(prefix +
                     "\"%s\" isn't a recognized divider type" % divider)
            raise stem.ProtocolError(
                "Unrecognized divider type '%s': %s" %
                (divider, stem.util.str_tools._to_unicode(line)))