add tor deps

This commit is contained in:
j 2015-11-23 22:13:53 +01:00
commit 1f23120cc3
91 changed files with 25537 additions and 535 deletions

View file

@ -0,0 +1,588 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# See LICENSE for licensing information
"""
Parses replies from the control socket.
**Module Overview:**
::
convert - translates a ControlMessage into a particular response subclass
ControlMessage - Message that's read from the control socket.
|- SingleLineResponse - Simple tor response only including a single line of information.
|
|- from_str - provides a ControlMessage for the given string
|- is_ok - response had a 250 status
|- content - provides the parsed message content
|- raw_content - unparsed socket data
|- __str__ - content stripped of protocol formatting
+- __iter__ - ControlLine entries for the content of the message
ControlLine - String subclass with methods for parsing controller responses.
|- remainder - provides the unparsed content
|- is_empty - checks if the remaining content is empty
|- is_next_quoted - checks if the next entry is a quoted value
|- is_next_mapping - checks if the next entry is a KEY=VALUE mapping
|- peek_key - provides the key of the next entry
|- pop - removes and returns the next entry
+- pop_mapping - removes and returns the next entry as a KEY=VALUE mapping
"""
__all__ = [
'add_onion',
'events',
'getinfo',
'getconf',
'protocolinfo',
'authchallenge',
'convert',
'ControlMessage',
'ControlLine',
'SingleLineResponse',
]
import re
import threading
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
import 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
CONTROL_ESCAPES = {r'\\': '\\', r'\"': '\"', r'\'': '\'',
r'\r': '\r', r'\n': '\n', r'\t': '\t'}
def convert(response_type, message, **kwargs):
"""
Converts a :class:`~stem.response.ControlMessage` into a particular kind of
tor response. This does an in-place conversion of the message from being a
:class:`~stem.response.ControlMessage` to a subclass for its response type.
Recognized types include...
=================== =====
response_type Class
=================== =====
**GETINFO** :class:`stem.response.getinfo.GetInfoResponse`
**GETCONF** :class:`stem.response.getconf.GetConfResponse`
**MAPADDRESS** :class:`stem.response.mapaddress.MapAddressResponse`
**EVENT** :class:`stem.response.events.Event` subclass
**PROTOCOLINFO** :class:`stem.response.protocolinfo.ProtocolInfoResponse`
**AUTHCHALLENGE** :class:`stem.response.authchallenge.AuthChallengeResponse`
**SINGLELINE** :class:`stem.response.SingleLineResponse`
=================== =====
:param str response_type: type of tor response to convert to
:param stem.response.ControlMessage message: message to be converted
:param kwargs: optional keyword arguments to be passed to the parser method
:raises:
* :class:`stem.ProtocolError` the message isn't a proper response of
that type
* :class:`stem.InvalidArguments` the arguments given as input are
invalid, this is can only be raised if the response_type is: **GETINFO**,
**GETCONF**
* :class:`stem.InvalidRequest` the arguments given as input are
invalid, this is can only be raised if the response_type is:
**MAPADDRESS**
* :class:`stem.OperationFailed` if the action the event represents failed,
this is can only be raised if the response_type is: **MAPADDRESS**
* **TypeError** if argument isn't a :class:`~stem.response.ControlMessage`
or response_type isn't supported
"""
import stem.response.add_onion
import stem.response.authchallenge
import stem.response.events
import stem.response.getinfo
import stem.response.getconf
import stem.response.mapaddress
import stem.response.protocolinfo
if not isinstance(message, ControlMessage):
raise TypeError('Only able to convert stem.response.ControlMessage instances')
response_types = {
'ADD_ONION': stem.response.add_onion.AddOnionResponse,
'AUTHCHALLENGE': stem.response.authchallenge.AuthChallengeResponse,
'EVENT': stem.response.events.Event,
'GETINFO': stem.response.getinfo.GetInfoResponse,
'GETCONF': stem.response.getconf.GetConfResponse,
'MAPADDRESS': stem.response.mapaddress.MapAddressResponse,
'SINGLELINE': SingleLineResponse,
'PROTOCOLINFO': stem.response.protocolinfo.ProtocolInfoResponse,
}
try:
response_class = response_types[response_type]
except TypeError:
raise TypeError('Unsupported response type: %s' % response_type)
message.__class__ = response_class
message._parse_message(**kwargs)
class ControlMessage(object):
"""
Message from the control socket. This is iterable and can be stringified for
individual message components stripped of protocol formatting. Messages are
never empty.
"""
@staticmethod
def from_str(content, msg_type = None, **kwargs):
"""
Provides a ControlMessage for the given content.
.. versionadded:: 1.1.0
:param str content: message to construct the message from
:param str msg_type: type of tor reply to parse the content as
:param kwargs: optional keyword arguments to be passed to the parser method
:returns: stem.response.ControlMessage instance
"""
msg = stem.socket.recv_message(StringIO(content))
if msg_type is not None:
convert(msg_type, msg, **kwargs)
return msg
def __init__(self, parsed_content, raw_content):
if not parsed_content:
raise ValueError("ControlMessages can't be empty")
self._parsed_content = parsed_content
self._raw_content = raw_content
def is_ok(self):
"""
Checks if any of our lines have a 250 response.
:returns: **True** if any lines have a 250 response code, **False** otherwise
"""
for code, _, _ in self._parsed_content:
if code == '250':
return True
return False
def content(self, get_bytes = False):
"""
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.
The **status_code** and **divider** are both strings (**bytes** in python
2.x and **unicode** in python 3.x). The **content** however is **bytes** if
**get_bytes** is **True**.
.. versionchanged:: 1.1.0
Added the get_bytes argument.
:param bool get_bytes: provides **bytes** for the **content** rather than a **str**
:returns: **list** of (str, str, str) tuples for the components of this message
"""
if stem.prereq.is_python_3() and not get_bytes:
return [(code, div, stem.util.str_tools._to_unicode(content)) for (code, div, content) in self._parsed_content]
else:
return list(self._parsed_content)
def raw_content(self, get_bytes = False):
"""
Provides the unparsed content read from the control socket.
.. versionchanged:: 1.1.0
Added the get_bytes argument.
:param bool get_bytes: if **True** then this provides **bytes** rather than a **str**
:returns: **str** of the socket data used to generate this message
"""
if stem.prereq.is_python_3() and not get_bytes:
return stem.util.str_tools._to_unicode(self._raw_content)
else:
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 :class:`~stem.response.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:
if stem.prereq.is_python_3():
content = stem.util.str_tools._to_unicode(content)
yield ControlLine(content)
def __len__(self):
"""
:returns: number of ControlLines
"""
return len(self._parsed_content)
def __getitem__(self, index):
"""
:returns: :class:`~stem.response.ControlLine` at the index
"""
content = self._parsed_content[index][2]
if stem.prereq.is_python_3():
content = stem.util.str_tools._to_unicode(content)
return 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.
:param bool escaped: 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_indices(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.
:param str key: checks that the key matches this value, skipping the check if **None**
:param bool quoted: checks that the mapping is to a quoted value
:param bool escaped: 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_indices(remainder, escaped)
return start_quote == key_match.end() and end_quote != -1
else:
return True # we just needed to check for the key
else:
return False # doesn't start with a key
def peek_key(self):
"""
Provides the key of the next entry, providing **None** if it isn't a
key/value mapping.
:returns: **str** with the next entry's key
"""
remainder = self._remainder
key_match = KEY_ARG.match(remainder)
if key_match:
return key_match.groups()[0]
else:
return None
def pop(self, quoted = False, escaped = False):
"""
Parses the next space separated entry, removing it and the space from our
remaining content. Examples...
::
>>> line = ControlLine("\\"We're all mad here.\\" says the grinning cat.")
>>> print line.pop(True)
"We're all mad here."
>>> print line.pop()
"says"
>>> print line.remainder()
"the grinning cat."
>>> line = ControlLine("\\"this has a \\\\\\" and \\\\\\\\ in it\\" foo=bar more_data")
>>> print line.pop(True, True)
"this has a \\" and \\\\ in it"
:param bool quoted: parses the next entry as a quoted value, removing the quotes
:param bool escaped: unescapes the CONTROL_ESCAPES escape sequences
:returns: **str** of the next space separated entry
:raises:
* **ValueError** if quoted is True without the value being quoted
* **IndexError** if we don't have any remaining content left to parse
"""
with self._remainder_lock:
next_entry, remainder = _parse_entry(self._remainder, quoted, escaped)
self._remainder = remainder
return next_entry
def pop_mapping(self, quoted = False, escaped = False):
"""
Parses the next space separated entry as a KEY=VALUE mapping, removing it
and the space from our remaining content.
:param bool quoted: parses the value as being quoted, removing the quotes
:param bool escaped: 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
:raises: **IndexError** if there's nothing to parse from the line
"""
with self._remainder_lock:
if self.is_empty():
raise IndexError('no remaining content to parse')
key_match = KEY_ARG.match(self._remainder)
if not key_match:
raise ValueError("the next entry isn't a KEY=VALUE mapping: " + self._remainder)
# parse off the key
key = key_match.groups()[0]
remainder = self._remainder[key_match.end():]
next_entry, remainder = _parse_entry(remainder, quoted, escaped)
self._remainder = remainder
return (key, next_entry)
def _parse_entry(line, quoted, escaped):
"""
Parses the next entry from the given space separated content.
:param str line: content to be parsed
:param bool quoted: parses the next entry as a quoted value, removing the quotes
:param bool escaped: 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_indices(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:
next_entry = _unescape(next_entry)
return (next_entry, remainder.lstrip())
def _get_quote_indices(line, escaped):
"""
Provides the indices of the next two quotes in the given content.
:param str line: content to be parsed
:param bool escaped: 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 _unescape(entry):
# Unescapes the given string with the mappings in CONTROL_ESCAPES.
#
# This can't be a simple series of str.replace() calls because replacements
# need to be excluded from consideration for further unescaping. For
# instance, '\\t' should be converted to '\t' rather than a tab.
def _pop_with_unescape(entry):
# Pop either the first character or the escape sequence conversion the
# entry starts with. This provides a tuple of...
#
# (unescaped prefix, remaining entry)
for esc_sequence, replacement in CONTROL_ESCAPES.items():
if entry.startswith(esc_sequence):
return (replacement, entry[len(esc_sequence):])
return (entry[0], entry[1:])
result = []
while entry:
prefix, entry = _pop_with_unescape(entry)
result.append(prefix)
return ''.join(result)
class SingleLineResponse(ControlMessage):
"""
Reply to a request that performs an action rather than querying data. These
requests only contain a single line, which is 'OK' if successful, and a
description of the problem if not.
:var str code: status code for our line
:var str message: content of the line
"""
def is_ok(self, strict = False):
"""
Checks if the response code is "250". If strict is **True** then this
checks if the response is "250 OK"
:param bool strict: checks for a "250 OK" message if **True**
:returns:
* If strict is **False**: **True** if the response code is "250", **False** otherwise
* If strict is **True**: **True** if the response is "250 OK", **False** otherwise
"""
if strict:
return self.content()[0] == ('250', ' ', 'OK')
return self.content()[0][0] == '250'
def _parse_message(self):
content = self.content()
if len(content) > 1:
raise stem.ProtocolError('Received multi-line response')
elif len(content) == 0:
raise stem.ProtocolError('Received empty response')
else:
self.code, _, self.message = content[0]

View file

@ -0,0 +1,43 @@
# Copyright 2015, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import stem.response
class AddOnionResponse(stem.response.ControlMessage):
"""
ADD_ONION response.
:var str service_id: hidden service address without the '.onion' suffix
:var str private_key: base64 encoded hidden service private key
:var str private_key_type: crypto used to generate the hidden service private
key (such as RSA1024)
"""
def _parse_message(self):
# Example:
# 250-ServiceID=gfzprpioee3hoppz
# 250-PrivateKey=RSA1024:MIICXgIBAAKBgQDZvYVxv...
# 250 OK
self.service_id = None
self.private_key = None
self.private_key_type = None
if not self.is_ok():
raise stem.ProtocolError("ADD_ONION response didn't have an OK status: %s" % self)
if not str(self).startswith('ServiceID='):
raise stem.ProtocolError('ADD_ONION response should start with the service id: %s' % self)
for line in list(self):
if '=' in line:
key, value = line.split('=', 1)
if key == 'ServiceID':
self.service_id = value
elif key == 'PrivateKey':
if ':' not in value:
raise stem.ProtocolError("ADD_ONION PrivateKey lines should be of the form 'PrivateKey=[type]:[key]: %s" % self)
self.private_key_type, self.private_key = value.split(':', 1)

View file

@ -0,0 +1,56 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import binascii
import stem.response
import stem.socket
import stem.util.str_tools
import stem.util.tor_tools
class AuthChallengeResponse(stem.response.ControlMessage):
"""
AUTHCHALLENGE query response.
:var str server_hash: server hash provided by tor
:var str server_nonce: server nonce provided by tor
"""
def _parse_message(self):
# Example:
# 250 AUTHCHALLENGE SERVERHASH=680A73C9836C4F557314EA1C4EDE54C285DB9DC89C83627401AEF9D7D27A95D5 SERVERNONCE=F8EA4B1F2C8B40EF1AF68860171605B910E3BBCABADF6FC3DB1FA064F4690E85
self.server_hash = None
self.server_nonce = None
if not self.is_ok():
raise stem.ProtocolError("AUTHCHALLENGE response didn't have an OK status:\n%s" % self)
elif len(self) > 1:
raise stem.ProtocolError('Received multiline AUTHCHALLENGE response:\n%s' % self)
line = self[0]
# sanity check that we're a AUTHCHALLENGE response
if not line.pop() == 'AUTHCHALLENGE':
raise stem.ProtocolError('Message is not an AUTHCHALLENGE response (%s)' % self)
if line.is_next_mapping('SERVERHASH'):
value = line.pop_mapping()[1]
if not stem.util.tor_tools.is_hex_digits(value, 64):
raise stem.ProtocolError('SERVERHASH has an invalid value: %s' % value)
self.server_hash = binascii.a2b_hex(stem.util.str_tools._to_bytes(value))
else:
raise stem.ProtocolError('Missing SERVERHASH mapping: %s' % line)
if line.is_next_mapping('SERVERNONCE'):
value = line.pop_mapping()[1]
if not stem.util.tor_tools.is_hex_digits(value, 64):
raise stem.ProtocolError('SERVERNONCE has an invalid value: %s' % value)
self.server_nonce = binascii.a2b_hex(stem.util.str_tools._to_bytes(value))
else:
raise stem.ProtocolError('Missing SERVERNONCE mapping: %s' % line)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,55 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import stem.response
import stem.socket
class GetConfResponse(stem.response.ControlMessage):
"""
Reply for a GETCONF query.
Note that configuration parameters won't match what we queried for if it's one
of the special mapping options (ex. 'HiddenServiceOptions').
:var dict entries: mapping between the config parameter (**str**) and their
values (**list** of **str**)
"""
def _parse_message(self):
# Example:
# 250-CookieAuthentication=0
# 250-ControlPort=9100
# 250-DataDirectory=/home/neena/.tor
# 250 DirPort
self.entries = {}
remaining_lines = list(self)
if self.content() == [('250', ' ', 'OK')]:
return
if not self.is_ok():
unrecognized_keywords = []
for code, _, line in self.content():
if code == '552' and line.startswith('Unrecognized configuration key "') and line.endswith('"'):
unrecognized_keywords.append(line[32:-1])
if unrecognized_keywords:
raise stem.InvalidArguments('552', 'GETCONF request contained unrecognized keywords: %s' % ', '.join(unrecognized_keywords), unrecognized_keywords)
else:
raise stem.ProtocolError('GETCONF response contained a non-OK status code:\n%s' % self)
while remaining_lines:
line = remaining_lines.pop(0)
if line.is_next_mapping():
key, value = line.split('=', 1)
else:
key, value = (line.pop(), None)
if key not in self.entries:
self.entries[key] = []
if value is not None:
self.entries[key].append(value)

View file

@ -0,0 +1,78 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import stem.response
import stem.socket
class GetInfoResponse(stem.response.ControlMessage):
"""
Reply for a GETINFO query.
:var dict entries: mapping between the queried options and their bytes values
"""
def _parse_message(self):
# Example:
# 250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)
# 250+config-text=
# ControlPort 9051
# DataDirectory /home/atagar/.tor
# ExitPolicy reject *:*
# Log notice stdout
# Nickname Unnamed
# ORPort 9050
# .
# 250 OK
self.entries = {}
remaining_lines = [content for (code, div, content) in self.content(get_bytes = True)]
if not self.is_ok() or not remaining_lines.pop() == b'OK':
unrecognized_keywords = []
for code, _, line in self.content():
if code == '552' and line.startswith('Unrecognized key "') and line.endswith('"'):
unrecognized_keywords.append(line[18:-1])
if unrecognized_keywords:
raise stem.InvalidArguments('552', 'GETINFO request contained unrecognized keywords: %s\n' % ', '.join(unrecognized_keywords), unrecognized_keywords)
else:
raise stem.ProtocolError("GETINFO response didn't have an OK status:\n%s" % self)
while remaining_lines:
try:
key, value = remaining_lines.pop(0).split(b'=', 1)
except ValueError:
raise stem.ProtocolError('GETINFO replies should only contain parameter=value mappings:\n%s' % self)
if stem.prereq.is_python_3():
key = stem.util.str_tools._to_unicode(key)
# if the value is a multiline value then it *must* be of the form
# '<key>=\n<value>'
if b'\n' in value:
if not value.startswith(b'\n'):
raise stem.ProtocolError("GETINFO response contained a multi-line value that didn't start with a newline:\n%s" % self)
value = value[1:]
self.entries[key] = value
def _assert_matches(self, params):
"""
Checks if we match a given set of parameters, and raise a ProtocolError if not.
:param set params: parameters to assert that we contain
:raises:
* :class:`stem.ProtocolError` if parameters don't match this response
"""
reply_params = set(self.entries.keys())
if params != reply_params:
requested_label = ', '.join(params)
reply_label = ', '.join(reply_params)
raise stem.ProtocolError("GETINFO reply doesn't match the parameters that we requested. Queried '%s' but got '%s'." % (requested_label, reply_label))

View file

@ -0,0 +1,42 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import stem.response
import stem.socket
class MapAddressResponse(stem.response.ControlMessage):
"""
Reply for a MAPADDRESS query.
Doesn't raise an exception unless no addresses were mapped successfully.
:var dict entries: mapping between the original and replacement addresses
:raises:
* :class:`stem.OperationFailed` if Tor was unable to satisfy the request
* :class:`stem.InvalidRequest` if the addresses provided were invalid
"""
def _parse_message(self):
# Example:
# 250-127.192.10.10=torproject.org
# 250 1.2.3.4=tor.freehaven.net
if not self.is_ok():
for code, _, message in self.content():
if code == '512':
raise stem.InvalidRequest(code, message)
elif code == '451':
raise stem.OperationFailed(code, message)
else:
raise stem.ProtocolError('MAPADDRESS returned unexpected response code: %s', code)
self.entries = {}
for code, _, message in self.content():
if code == '250':
try:
key, value = message.split('=', 1)
self.entries[key] = value
except ValueError:
raise stem.ProtocolError(None, "MAPADDRESS returned '%s', which isn't a mapping" % message)

View file

@ -0,0 +1,122 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import stem.response
import stem.socket
import stem.version
from stem.connection import AuthMethod
from stem.util import log
class ProtocolInfoResponse(stem.response.ControlMessage):
"""
Version one PROTOCOLINFO query response.
The protocol_version is the only mandatory data for a valid PROTOCOLINFO
response, so all other values are None if undefined or empty if a collection.
:var int protocol_version: protocol version of the response
:var stem.version.Version tor_version: version of the tor process
:var tuple auth_methods: :data:`stem.connection.AuthMethod` types that tor will accept
:var tuple unknown_auth_methods: strings of unrecognized auth methods
:var str cookie_path: path of tor's authentication cookie
"""
def _parse_message(self):
# Example:
# 250-PROTOCOLINFO 1
# 250-AUTH METHODS=COOKIE COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
# 250-VERSION Tor="0.2.1.30"
# 250 OK
self.protocol_version = None
self.tor_version = None
self.auth_methods = ()
self.unknown_auth_methods = ()
self.cookie_path = None
auth_methods, unknown_auth_methods = [], []
remaining_lines = list(self)
if not self.is_ok() or not remaining_lines.pop() == 'OK':
raise stem.ProtocolError("PROTOCOLINFO response didn't have an OK status:\n%s" % self)
# sanity check that we're a PROTOCOLINFO response
if not remaining_lines[0].startswith('PROTOCOLINFO'):
raise stem.ProtocolError('Message is not a PROTOCOLINFO response:\n%s' % self)
while remaining_lines:
line = remaining_lines.pop(0)
line_type = line.pop()
if line_type == 'PROTOCOLINFO':
# Line format:
# FirstLine = "PROTOCOLINFO" SP PIVERSION CRLF
# PIVERSION = 1*DIGIT
if line.is_empty():
raise stem.ProtocolError("PROTOCOLINFO response's initial line is missing the protocol version: %s" % line)
try:
self.protocol_version = int(line.pop())
except ValueError:
raise stem.ProtocolError('PROTOCOLINFO response version is non-numeric: %s' % line)
# The piversion really should be '1' but, according to the spec, tor
# does not necessarily need to provide the PROTOCOLINFO version that we
# requested. Log if it's something we aren't expecting but still make
# an effort to parse like a v1 response.
if self.protocol_version != 1:
log.info("We made a PROTOCOLINFO version 1 query but got a version %i response instead. We'll still try to use it, but this may cause problems." % self.protocol_version)
elif line_type == 'AUTH':
# Line format:
# AuthLine = "250-AUTH" SP "METHODS=" AuthMethod *("," AuthMethod)
# *(SP "COOKIEFILE=" AuthCookieFile) CRLF
# AuthMethod = "NULL" / "HASHEDPASSWORD" / "COOKIE"
# AuthCookieFile = QuotedString
# parse AuthMethod mapping
if not line.is_next_mapping('METHODS'):
raise stem.ProtocolError("PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line)
for method in line.pop_mapping()[1].split(','):
if method == 'NULL':
auth_methods.append(AuthMethod.NONE)
elif method == 'HASHEDPASSWORD':
auth_methods.append(AuthMethod.PASSWORD)
elif method == 'COOKIE':
auth_methods.append(AuthMethod.COOKIE)
elif method == 'SAFECOOKIE':
auth_methods.append(AuthMethod.SAFECOOKIE)
else:
unknown_auth_methods.append(method)
message_id = 'stem.response.protocolinfo.unknown_auth_%s' % method
log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %s" % method)
# our auth_methods should have a single AuthMethod.UNKNOWN entry if
# any unknown authentication methods exist
if AuthMethod.UNKNOWN not in auth_methods:
auth_methods.append(AuthMethod.UNKNOWN)
# parse optional COOKIEFILE mapping (quoted and can have escapes)
if line.is_next_mapping('COOKIEFILE', True, True):
self.cookie_path = line.pop_mapping(True, True)[1]
elif line_type == 'VERSION':
# Line format:
# VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF
# TorVersion = QuotedString
if not line.is_next_mapping('Tor', True):
raise stem.ProtocolError("PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line)
try:
self.tor_version = stem.version.Version(line.pop_mapping(True)[1])
except ValueError as exc:
raise stem.ProtocolError(exc)
else:
log.debug("Unrecognized PROTOCOLINFO line type '%s', ignoring it: %s" % (line_type, line))
self.auth_methods = tuple(auth_methods)
self.unknown_auth_methods = tuple(unknown_auth_methods)