mirror of
https://github.com/onionshare/onionshare.git
synced 2025-07-31 18:38:57 -04:00
added stem python library
This commit is contained in:
parent
8ffa569094
commit
619ab6db0f
37 changed files with 19032 additions and 0 deletions
571
lib/stem/response/__init__.py
Normal file
571
lib/stem/response/__init__.py
Normal file
|
@ -0,0 +1,571 @@
|
|||
# Copyright 2012-2013, 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.
|
||||
|- from_str - provides a ControlMessage for the given string
|
||||
|- 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
|
||||
|
||||
SingleLineResponse - Simple tor response only including a single line of information.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"events",
|
||||
"getinfo",
|
||||
"getconf",
|
||||
"protocolinfo",
|
||||
"authchallenge",
|
||||
"convert",
|
||||
"ControlMessage",
|
||||
"ControlLine",
|
||||
"SingleLineResponse",
|
||||
]
|
||||
|
||||
import re
|
||||
import StringIO
|
||||
import threading
|
||||
|
||||
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.events
|
||||
import stem.response.getinfo
|
||||
import stem.response.getconf
|
||||
import stem.response.protocolinfo
|
||||
import stem.response.authchallenge
|
||||
import stem.response.mapaddress
|
||||
|
||||
if not isinstance(message, ControlMessage):
|
||||
raise TypeError("Only able to convert stem.response.ControlMessage instances")
|
||||
|
||||
response_types = {
|
||||
"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,
|
||||
"AUTHCHALLENGE": stem.response.authchallenge.AuthChallengeResponse,
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
: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.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**.
|
||||
|
||||
: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.
|
||||
|
||||
: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]
|
56
lib/stem/response/authchallenge.py
Normal file
56
lib/stem/response/authchallenge.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
# Copyright 2012-2013, 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)
|
945
lib/stem/response/events.py
Normal file
945
lib/stem/response/events.py
Normal file
|
@ -0,0 +1,945 @@
|
|||
# Copyright 2012-2013, Damian Johnson and The Tor Project
|
||||
# See LICENSE for licensing information
|
||||
|
||||
import datetime
|
||||
import io
|
||||
import re
|
||||
import time
|
||||
|
||||
import stem
|
||||
import stem.control
|
||||
import stem.descriptor.router_status_entry
|
||||
import stem.response
|
||||
import stem.version
|
||||
|
||||
from stem.util import connection, log, str_tools, tor_tools
|
||||
|
||||
# Matches keyword=value arguments. This can't be a simple "(.*)=(.*)" pattern
|
||||
# because some positional arguments, like circuit paths, can have an equal
|
||||
# sign.
|
||||
|
||||
KW_ARG = re.compile("^(.*) ([A-Za-z0-9_]+)=(\S*)$")
|
||||
QUOTED_KW_ARG = re.compile("^(.*) ([A-Za-z0-9_]+)=\"(.*)\"$")
|
||||
|
||||
|
||||
class Event(stem.response.ControlMessage):
|
||||
"""
|
||||
Base for events we receive asynchronously, as described in section 4.1 of the
|
||||
`control-spec
|
||||
<https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_.
|
||||
|
||||
:var str type: event type
|
||||
:var int arrived_at: unix timestamp for when the message arrived
|
||||
:var list positional_args: positional arguments of the event
|
||||
:var dict keyword_args: key/value arguments of the event
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = () # attribute names for recognized positional arguments
|
||||
_KEYWORD_ARGS = {} # map of 'keyword => attribute' for recognized attributes
|
||||
_QUOTED = () # positional arguments that are quoted
|
||||
_OPTIONALLY_QUOTED = () # positional arguments that may or may not be quoted
|
||||
_SKIP_PARSING = False # skip parsing contents into our positional_args and keyword_args
|
||||
_VERSION_ADDED = stem.version.Version('0.1.1.1-alpha') # minimum version with control-spec V1 event support
|
||||
|
||||
def _parse_message(self, arrived_at = None):
|
||||
if arrived_at is None:
|
||||
arrived_at = int(time.time())
|
||||
|
||||
if not str(self).strip():
|
||||
raise stem.ProtocolError("Received a blank tor event. Events must at the very least have a type.")
|
||||
|
||||
self.type = str(self).split().pop(0)
|
||||
self.arrived_at = arrived_at
|
||||
|
||||
# if we're a recognized event type then translate ourselves into that subclass
|
||||
|
||||
if self.type in EVENT_TYPE_TO_CLASS:
|
||||
self.__class__ = EVENT_TYPE_TO_CLASS[self.type]
|
||||
|
||||
self.positional_args = []
|
||||
self.keyword_args = {}
|
||||
|
||||
if not self._SKIP_PARSING:
|
||||
self._parse_standard_attr()
|
||||
|
||||
self._parse()
|
||||
|
||||
def _parse_standard_attr(self):
|
||||
"""
|
||||
Most events are of the form...
|
||||
650 *( positional_args ) *( key "=" value )
|
||||
|
||||
This parses this standard format, populating our **positional_args** and
|
||||
**keyword_args** attributes and creating attributes if it's in our event's
|
||||
**_POSITIONAL_ARGS** and **_KEYWORD_ARGS**.
|
||||
"""
|
||||
|
||||
# Tor events contain some number of positional arguments followed by
|
||||
# key/value mappings. Parsing keyword arguments from the end until we hit
|
||||
# something that isn't a key/value mapping. The rest are positional.
|
||||
|
||||
content = str(self)
|
||||
|
||||
while True:
|
||||
match = QUOTED_KW_ARG.match(content)
|
||||
|
||||
if not match:
|
||||
match = KW_ARG.match(content)
|
||||
|
||||
if match:
|
||||
content, keyword, value = match.groups()
|
||||
self.keyword_args[keyword] = value
|
||||
else:
|
||||
break
|
||||
|
||||
# Setting attributes for the fields that we recognize.
|
||||
|
||||
self.positional_args = content.split()[1:]
|
||||
positional = list(self.positional_args)
|
||||
|
||||
for attr_name in self._POSITIONAL_ARGS:
|
||||
attr_value = None
|
||||
|
||||
if positional:
|
||||
if attr_name in self._QUOTED or (attr_name in self._OPTIONALLY_QUOTED and positional[0].startswith('"')):
|
||||
attr_values = [positional.pop(0)]
|
||||
|
||||
if not attr_values[0].startswith('"'):
|
||||
raise stem.ProtocolError("The %s value should be quoted, but didn't have a starting quote: %s" % (attr_name, self))
|
||||
|
||||
while True:
|
||||
if not positional:
|
||||
raise stem.ProtocolError("The %s value should be quoted, but didn't have an ending quote: %s" % (attr_name, self))
|
||||
|
||||
attr_values.append(positional.pop(0))
|
||||
|
||||
if attr_values[-1].endswith('"'):
|
||||
break
|
||||
|
||||
attr_value = " ".join(attr_values)[1:-1]
|
||||
else:
|
||||
attr_value = positional.pop(0)
|
||||
|
||||
setattr(self, attr_name, attr_value)
|
||||
|
||||
for controller_attr_name, attr_name in self._KEYWORD_ARGS.items():
|
||||
setattr(self, attr_name, self.keyword_args.get(controller_attr_name))
|
||||
|
||||
# method overwritten by our subclasses for special handling that they do
|
||||
def _parse(self):
|
||||
pass
|
||||
|
||||
def _log_if_unrecognized(self, attr, attr_enum):
|
||||
"""
|
||||
Checks if an attribute exists in a given enumeration, logging a message if
|
||||
it isn't. Attributes can either be for a string or collection of strings
|
||||
|
||||
:param str attr: name of the attribute to check
|
||||
:param stem.util.enum.Enum enum: enumeration to check against
|
||||
"""
|
||||
|
||||
attr_values = getattr(self, attr)
|
||||
|
||||
if attr_values:
|
||||
if isinstance(attr_values, (bytes, unicode)):
|
||||
attr_values = [attr_values]
|
||||
|
||||
for value in attr_values:
|
||||
if not value in attr_enum:
|
||||
log_id = "event.%s.unknown_%s.%s" % (self.type.lower(), attr, value)
|
||||
unrecognized_msg = "%s event had an unrecognized %s (%s). Maybe a new addition to the control protocol? Full Event: '%s'" % (self.type, attr, value, self)
|
||||
log.log_once(log_id, log.INFO, unrecognized_msg)
|
||||
|
||||
|
||||
class AddrMapEvent(Event):
|
||||
"""
|
||||
Event that indicates a new address mapping.
|
||||
|
||||
The ADDRMAP event was one of the first Control Protocol V1 events and was
|
||||
introduced in tor version 0.1.1.1-alpha.
|
||||
|
||||
:var str hostname: address being resolved
|
||||
:var str destination: destionation of the resolution, this is usually an ip,
|
||||
but could be a hostname if TrackHostExits is enabled or **NONE** if the
|
||||
resolution failed
|
||||
:var datetime expiry: expiration time of the resolution in local time
|
||||
:var str error: error code if the resolution failed
|
||||
:var datetime utc_expiry: expiration time of the resolution in UTC
|
||||
:var bool cached: **True** if the resolution will be kept until it expires,
|
||||
**False** otherwise or **None** if undefined
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = ("hostname", "destination", "expiry")
|
||||
_KEYWORD_ARGS = {
|
||||
"error": "error",
|
||||
"EXPIRES": "utc_expiry",
|
||||
"CACHED": "cached",
|
||||
}
|
||||
_OPTIONALLY_QUOTED = ("expiry")
|
||||
|
||||
def _parse(self):
|
||||
if self.destination == "<error>":
|
||||
self.destination = None
|
||||
|
||||
if self.expiry is not None:
|
||||
if self.expiry == "NEVER":
|
||||
self.expiry = None
|
||||
else:
|
||||
try:
|
||||
self.expiry = datetime.datetime.strptime(self.expiry, "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
raise stem.ProtocolError("Unable to parse date in ADDRMAP event: %s" % self)
|
||||
|
||||
if self.utc_expiry is not None:
|
||||
self.utc_expiry = datetime.datetime.strptime(self.utc_expiry, "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if self.cached is not None:
|
||||
if self.cached == "YES":
|
||||
self.cached = True
|
||||
elif self.cached == "NO":
|
||||
self.cached = False
|
||||
else:
|
||||
raise stem.ProtocolError("An ADDRMAP event's CACHED mapping can only be 'YES' or 'NO': %s" % self)
|
||||
|
||||
|
||||
class AuthDirNewDescEvent(Event):
|
||||
"""
|
||||
Event specific to directory authorities, indicating that we just received new
|
||||
descriptors. The descriptor type contained within this event is unspecified
|
||||
so the descriptor contents are left unparsed.
|
||||
|
||||
The AUTHDIR_NEWDESCS event was introduced in tor version 0.1.1.10-alpha.
|
||||
|
||||
:var stem.AuthDescriptorAction action: what is being done with the descriptor
|
||||
:var str message: explanation of why we chose this action
|
||||
:var str descriptor: content of the descriptor
|
||||
"""
|
||||
|
||||
_SKIP_PARSING = True
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_AUTHDIR_NEWDESCS
|
||||
|
||||
def _parse(self):
|
||||
lines = str(self).split('\n')
|
||||
|
||||
if len(lines) < 5:
|
||||
raise stem.ProtocolError("AUTHDIR_NEWDESCS events must contain lines for at least the type, action, message, descriptor, and terminating 'OK'")
|
||||
elif not lines[-1] == "OK":
|
||||
raise stem.ProtocolError("AUTHDIR_NEWDESCS doesn't end with an 'OK'")
|
||||
|
||||
self.action = lines[1]
|
||||
self.message = lines[2]
|
||||
self.descriptor = '\n'.join(lines[3:-1])
|
||||
|
||||
|
||||
class BandwidthEvent(Event):
|
||||
"""
|
||||
Event emitted every second with the bytes sent and received by tor.
|
||||
|
||||
The BW event was one of the first Control Protocol V1 events and was
|
||||
introduced in tor version 0.1.1.1-alpha.
|
||||
|
||||
:var long read: bytes received by tor that second
|
||||
:var long written: bytes sent by tor that second
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = ("read", "written")
|
||||
|
||||
def _parse(self):
|
||||
if not self.read:
|
||||
raise stem.ProtocolError("BW event is missing its read value")
|
||||
elif not self.written:
|
||||
raise stem.ProtocolError("BW event is missing its written value")
|
||||
elif not self.read.isdigit() or not self.written.isdigit():
|
||||
raise stem.ProtocolError("A BW event's bytes sent and received should be a positive numeric value, received: %s" % self)
|
||||
|
||||
self.read = long(self.read)
|
||||
self.written = long(self.written)
|
||||
|
||||
|
||||
class BuildTimeoutSetEvent(Event):
|
||||
"""
|
||||
Event indicating that the timeout value for a circuit has changed. This was
|
||||
first added in tor version 0.2.2.7.
|
||||
|
||||
The BUILDTIMEOUT_SET event was introduced in tor version 0.2.2.7-alpha.
|
||||
|
||||
:var stem.TimeoutSetType set_type: way in which the timeout is changing
|
||||
:var int total_times: circuit build times tor used to determine the timeout
|
||||
:var int timeout: circuit timeout value in milliseconds
|
||||
:var int xm: Pareto parameter Xm in milliseconds
|
||||
:var float alpha: Pareto parameter alpha
|
||||
:var float quantile: CDF quantile cutoff point
|
||||
:var float timeout_rate: ratio of circuits that have time out
|
||||
:var int close_timeout: duration to keep measurement circuits in milliseconds
|
||||
:var float close_rate: ratio of measurement circuits that are closed
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = ("set_type",)
|
||||
_KEYWORD_ARGS = {
|
||||
"TOTAL_TIMES": "total_times",
|
||||
"TIMEOUT_MS": "timeout",
|
||||
"XM": "xm",
|
||||
"ALPHA": "alpha",
|
||||
"CUTOFF_QUANTILE": "quantile",
|
||||
"TIMEOUT_RATE": "timeout_rate",
|
||||
"CLOSE_MS": "close_timeout",
|
||||
"CLOSE_RATE": "close_rate",
|
||||
}
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_BUILDTIMEOUT_SET
|
||||
|
||||
def _parse(self):
|
||||
# convert our integer and float parameters
|
||||
|
||||
for param in ('total_times', 'timeout', 'xm', 'close_timeout'):
|
||||
param_value = getattr(self, param)
|
||||
|
||||
if param_value is not None:
|
||||
try:
|
||||
setattr(self, param, int(param_value))
|
||||
except ValueError:
|
||||
raise stem.ProtocolError("The %s of a BUILDTIMEOUT_SET should be an integer: %s" % (param, self))
|
||||
|
||||
for param in ('alpha', 'quantile', 'timeout_rate', 'close_rate'):
|
||||
param_value = getattr(self, param)
|
||||
|
||||
if param_value is not None:
|
||||
try:
|
||||
setattr(self, param, float(param_value))
|
||||
except ValueError:
|
||||
raise stem.ProtocolError("The %s of a BUILDTIMEOUT_SET should be a float: %s" % (param, self))
|
||||
|
||||
self._log_if_unrecognized('set_type', stem.TimeoutSetType)
|
||||
|
||||
|
||||
class CircuitEvent(Event):
|
||||
"""
|
||||
Event that indicates that a circuit has changed.
|
||||
|
||||
The fingerprint or nickname values in our 'path' may be **None** if the
|
||||
VERBOSE_NAMES feature isn't enabled. The option was first introduced in tor
|
||||
version 0.1.2.2, and on by default after 0.2.2.1.
|
||||
|
||||
The CIRC event was one of the first Control Protocol V1 events and was
|
||||
introduced in tor version 0.1.1.1-alpha.
|
||||
|
||||
:var str id: circuit identifier
|
||||
:var stem.CircStatus status: reported status for the circuit
|
||||
:var tuple path: relays involved in the circuit, these are
|
||||
**(fingerprint, nickname)** tuples
|
||||
:var tuple build_flags: :data:`~stem.CircBuildFlag` attributes
|
||||
governing how the circuit is built
|
||||
:var stem.CircPurpose purpose: purpose that the circuit is intended for
|
||||
:var stem.HiddenServiceState hs_state: status if this is a hidden service circuit
|
||||
:var str rend_query: circuit's rendezvous-point if this is hidden service related
|
||||
:var datetime created: time when the circuit was created or cannibalized
|
||||
:var stem.CircClosureReason reason: reason for the circuit to be closed
|
||||
:var stem.CircClosureReason remote_reason: remote side's reason for the circuit to be closed
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = ("id", "status", "path")
|
||||
_KEYWORD_ARGS = {
|
||||
"BUILD_FLAGS": "build_flags",
|
||||
"PURPOSE": "purpose",
|
||||
"HS_STATE": "hs_state",
|
||||
"REND_QUERY": "rend_query",
|
||||
"TIME_CREATED": "created",
|
||||
"REASON": "reason",
|
||||
"REMOTE_REASON": "remote_reason",
|
||||
}
|
||||
|
||||
def _parse(self):
|
||||
self.path = tuple(stem.control._parse_circ_path(self.path))
|
||||
|
||||
if self.build_flags is not None:
|
||||
self.build_flags = tuple(self.build_flags.split(','))
|
||||
|
||||
if self.created is not None:
|
||||
try:
|
||||
self.created = str_tools._parse_iso_timestamp(self.created)
|
||||
except ValueError as exc:
|
||||
raise stem.ProtocolError("Unable to parse create date (%s): %s" % (exc, self))
|
||||
|
||||
if not tor_tools.is_valid_circuit_id(self.id):
|
||||
raise stem.ProtocolError("Circuit IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self))
|
||||
|
||||
self._log_if_unrecognized('status', stem.CircStatus)
|
||||
self._log_if_unrecognized('build_flags', stem.CircBuildFlag)
|
||||
self._log_if_unrecognized('purpose', stem.CircPurpose)
|
||||
self._log_if_unrecognized('hs_state', stem.HiddenServiceState)
|
||||
self._log_if_unrecognized('reason', stem.CircClosureReason)
|
||||
self._log_if_unrecognized('remote_reason', stem.CircClosureReason)
|
||||
|
||||
|
||||
class CircMinorEvent(Event):
|
||||
"""
|
||||
Event providing information about minor changes in our circuits. This was
|
||||
first added in tor version 0.2.3.11.
|
||||
|
||||
The CIRC_MINOR event was introduced in tor version 0.2.3.11-alpha.
|
||||
|
||||
:var str id: circuit identifier
|
||||
:var stem.CircEvent event: type of change in the circuit
|
||||
:var tuple path: relays involved in the circuit, these are
|
||||
**(fingerprint, nickname)** tuples
|
||||
:var tuple build_flags: :data:`~stem.CircBuildFlag` attributes
|
||||
governing how the circuit is built
|
||||
:var stem.CircPurpose purpose: purpose that the circuit is intended for
|
||||
:var stem.HiddenServiceState hs_state: status if this is a hidden service circuit
|
||||
:var str rend_query: circuit's rendezvous-point if this is hidden service related
|
||||
:var datetime created: time when the circuit was created or cannibalized
|
||||
:var stem.CircPurpose old_purpose: prior purpose for the circuit
|
||||
:var stem.HiddenServiceState old_hs_state: prior status as a hidden service circuit
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = ("id", "event", "path")
|
||||
_KEYWORD_ARGS = {
|
||||
"BUILD_FLAGS": "build_flags",
|
||||
"PURPOSE": "purpose",
|
||||
"HS_STATE": "hs_state",
|
||||
"REND_QUERY": "rend_query",
|
||||
"TIME_CREATED": "created",
|
||||
"OLD_PURPOSE": "old_purpose",
|
||||
"OLD_HS_STATE": "old_hs_state",
|
||||
}
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_CIRC_MINOR
|
||||
|
||||
def _parse(self):
|
||||
self.path = tuple(stem.control._parse_circ_path(self.path))
|
||||
|
||||
if self.build_flags is not None:
|
||||
self.build_flags = tuple(self.build_flags.split(','))
|
||||
|
||||
if self.created is not None:
|
||||
try:
|
||||
self.created = str_tools._parse_iso_timestamp(self.created)
|
||||
except ValueError as exc:
|
||||
raise stem.ProtocolError("Unable to parse create date (%s): %s" % (exc, self))
|
||||
|
||||
if not tor_tools.is_valid_circuit_id(self.id):
|
||||
raise stem.ProtocolError("Circuit IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self))
|
||||
|
||||
self._log_if_unrecognized('event', stem.CircEvent)
|
||||
self._log_if_unrecognized('build_flags', stem.CircBuildFlag)
|
||||
self._log_if_unrecognized('purpose', stem.CircPurpose)
|
||||
self._log_if_unrecognized('hs_state', stem.HiddenServiceState)
|
||||
self._log_if_unrecognized('old_purpose', stem.CircPurpose)
|
||||
self._log_if_unrecognized('old_hs_state', stem.HiddenServiceState)
|
||||
|
||||
|
||||
class ClientsSeenEvent(Event):
|
||||
"""
|
||||
Periodic event on bridge relays that provides a summary of our users.
|
||||
|
||||
The CLIENTS_SEEN event was introduced in tor version 0.2.1.10-alpha.
|
||||
|
||||
:var datetime start_time: time in UTC that we started collecting these stats
|
||||
:var dict locales: mapping of country codes to a rounded count for the number of users
|
||||
:var dict ip_versions: mapping of ip protocols to a rounded count for the number of users
|
||||
"""
|
||||
|
||||
_KEYWORD_ARGS = {
|
||||
"TimeStarted": "start_time",
|
||||
"CountrySummary": "locales",
|
||||
"IPVersions": "ip_versions",
|
||||
}
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_CLIENTS_SEEN
|
||||
|
||||
def _parse(self):
|
||||
if self.start_time is not None:
|
||||
self.start_time = datetime.datetime.strptime(self.start_time, "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if self.locales is not None:
|
||||
locale_to_count = {}
|
||||
|
||||
for entry in self.locales.split(','):
|
||||
if not '=' in entry:
|
||||
raise stem.ProtocolError("The CLIENTS_SEEN's CountrySummary should be a comma separated listing of '<locale>=<count>' mappings: %s" % self)
|
||||
|
||||
locale, count = entry.split('=', 1)
|
||||
|
||||
if len(locale) != 2:
|
||||
raise stem.ProtocolError("Locales should be a two character code, got '%s': %s" % (locale, self))
|
||||
elif not count.isdigit():
|
||||
raise stem.ProtocolError("Locale count was non-numeric (%s): %s" % (count, self))
|
||||
elif locale in locale_to_count:
|
||||
raise stem.ProtocolError("CountrySummary had multiple mappings for '%s': %s" % (locale, self))
|
||||
|
||||
locale_to_count[locale] = int(count)
|
||||
|
||||
self.locales = locale_to_count
|
||||
|
||||
if self.ip_versions is not None:
|
||||
protocol_to_count = {}
|
||||
|
||||
for entry in self.ip_versions.split(','):
|
||||
if not '=' in entry:
|
||||
raise stem.ProtocolError("The CLIENTS_SEEN's IPVersions should be a comma separated listing of '<protocol>=<count>' mappings: %s" % self)
|
||||
|
||||
protocol, count = entry.split('=', 1)
|
||||
|
||||
if not count.isdigit():
|
||||
raise stem.ProtocolError("IP protocol count was non-numeric (%s): %s" % (count, self))
|
||||
|
||||
protocol_to_count[protocol] = int(count)
|
||||
|
||||
self.ip_versions = protocol_to_count
|
||||
|
||||
|
||||
class ConfChangedEvent(Event):
|
||||
"""
|
||||
Event that indicates that our configuration changed, either in response to a
|
||||
SETCONF or RELOAD signal.
|
||||
|
||||
The CONF_CHANGED event was introduced in tor version 0.2.3.3-alpha.
|
||||
|
||||
:var dict config: mapping of configuration options to their new values
|
||||
(**None** if the option is being unset)
|
||||
"""
|
||||
|
||||
_SKIP_PARSING = True
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_CONF_CHANGED
|
||||
|
||||
def _parse(self):
|
||||
self.config = {}
|
||||
|
||||
# Skip first and last line since they're the header and footer. For
|
||||
# instance...
|
||||
#
|
||||
# 650-CONF_CHANGED
|
||||
# 650-ExitNodes=caerSidi
|
||||
# 650-ExitPolicy
|
||||
# 650-MaxCircuitDirtiness=20
|
||||
# 650 OK
|
||||
|
||||
for line in str(self).splitlines()[1:-1]:
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
else:
|
||||
key, value = line, None
|
||||
|
||||
self.config[key] = value
|
||||
|
||||
|
||||
class DescChangedEvent(Event):
|
||||
"""
|
||||
Event that indicates that our descriptor has changed.
|
||||
|
||||
The DESCCHANGED event was introduced in tor version 0.1.2.2-alpha.
|
||||
"""
|
||||
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_DESCCHANGED
|
||||
|
||||
|
||||
class GuardEvent(Event):
|
||||
"""
|
||||
Event that indicates that our guard relays have changed. The 'endpoint' could
|
||||
be either a...
|
||||
|
||||
* fingerprint
|
||||
* 'fingerprint=nickname' pair
|
||||
|
||||
The derived 'endpoint_*' attributes are generally more useful.
|
||||
|
||||
The GUARD event was introduced in tor version 0.1.2.5-alpha.
|
||||
|
||||
:var stem.GuardType guard_type: purpose the guard relay is for
|
||||
:var str endpoint: relay that the event concerns
|
||||
:var str endpoint_fingerprint: endpoint's finterprint
|
||||
:var str endpoint_nickname: endpoint's nickname if it was provided
|
||||
:var stem.GuardStatus status: status of the guard relay
|
||||
"""
|
||||
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_GUARD
|
||||
_POSITIONAL_ARGS = ("guard_type", "endpoint", "status")
|
||||
|
||||
def _parse(self):
|
||||
self.endpoint_fingerprint = None
|
||||
self.endpoint_nickname = None
|
||||
|
||||
try:
|
||||
self.endpoint_fingerprint, self.endpoint_nickname = \
|
||||
stem.control._parse_circ_entry(self.endpoint)
|
||||
except stem.ProtocolError:
|
||||
raise stem.ProtocolError("ORCONN's endpoint doesn't match a ServerSpec: %s" % self)
|
||||
|
||||
self._log_if_unrecognized('guard_type', stem.GuardType)
|
||||
self._log_if_unrecognized('status', stem.GuardStatus)
|
||||
|
||||
|
||||
class LogEvent(Event):
|
||||
"""
|
||||
Tor logging event. These are the most visible kind of event since, by
|
||||
default, tor logs at the NOTICE :data:`~stem.Runlevel` to stdout.
|
||||
|
||||
The logging events were some of the first Control Protocol V1 events
|
||||
and were introduced in tor version 0.1.1.1-alpha.
|
||||
|
||||
:var stem.Runlevel runlevel: runlevel of the logged message
|
||||
:var str message: logged message
|
||||
"""
|
||||
|
||||
_SKIP_PARSING = True
|
||||
|
||||
def _parse(self):
|
||||
self.runlevel = self.type
|
||||
self._log_if_unrecognized('runlevel', stem.Runlevel)
|
||||
|
||||
# message is our content, minus the runlevel and ending "OK" if a
|
||||
# multi-line message
|
||||
|
||||
self.message = str(self)[len(self.runlevel) + 1:].rstrip("\nOK")
|
||||
|
||||
|
||||
class NetworkStatusEvent(Event):
|
||||
"""
|
||||
Event for when our copy of the consensus has changed. This was introduced in
|
||||
tor version 0.1.2.3.
|
||||
|
||||
The NS event was introduced in tor version 0.1.2.3-alpha.
|
||||
|
||||
:var list desc: :class:`~stem.descriptor.router_status_entry.RouterStatusEntryV3` for the changed descriptors
|
||||
"""
|
||||
|
||||
_SKIP_PARSING = True
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_NS
|
||||
|
||||
def _parse(self):
|
||||
content = str(self).lstrip("NS\n").rstrip("\nOK")
|
||||
|
||||
self.desc = list(stem.descriptor.router_status_entry._parse_file(
|
||||
io.BytesIO(str_tools._to_bytes(content)),
|
||||
True,
|
||||
entry_class = stem.descriptor.router_status_entry.RouterStatusEntryV3,
|
||||
))
|
||||
|
||||
|
||||
class NewConsensusEvent(Event):
|
||||
"""
|
||||
Event for when we have a new consensus. This is similar to
|
||||
:class:`~stem.response.events.NetworkStatusEvent`, except that it contains
|
||||
the whole consensus so anything not listed is implicitly no longer
|
||||
recommended.
|
||||
|
||||
The NEWCONSENSUS event was introduced in tor version 0.2.1.13-alpha.
|
||||
|
||||
:var list desc: :class:`~stem.descriptor.router_status_entry.RouterStatusEntryV3` for the changed descriptors
|
||||
"""
|
||||
|
||||
_SKIP_PARSING = True
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_NEWCONSENSUS
|
||||
|
||||
def _parse(self):
|
||||
content = str(self).lstrip("NEWCONSENSUS\n").rstrip("\nOK")
|
||||
|
||||
self.desc = list(stem.descriptor.router_status_entry._parse_file(
|
||||
io.BytesIO(str_tools._to_bytes(content)),
|
||||
True,
|
||||
entry_class = stem.descriptor.router_status_entry.RouterStatusEntryV3,
|
||||
))
|
||||
|
||||
|
||||
class NewDescEvent(Event):
|
||||
"""
|
||||
Event that indicates that a new descriptor is available.
|
||||
|
||||
The fingerprint or nickname values in our 'relays' may be **None** if the
|
||||
VERBOSE_NAMES feature isn't enabled. The option was first introduced in tor
|
||||
version 0.1.2.2, and on by default after 0.2.2.1.
|
||||
|
||||
The NEWDESC event was one of the first Control Protocol V1 events and was
|
||||
introduced in tor version 0.1.1.1-alpha.
|
||||
|
||||
:var tuple relays: **(fingerprint, nickname)** tuples for the relays with
|
||||
new descriptors
|
||||
"""
|
||||
|
||||
def _parse(self):
|
||||
self.relays = tuple([stem.control._parse_circ_entry(entry) for entry in str(self).split()[1:]])
|
||||
|
||||
|
||||
class ORConnEvent(Event):
|
||||
"""
|
||||
Event that indicates a change in a relay connection. The 'endpoint' could be
|
||||
any of several things including a...
|
||||
|
||||
* fingerprint
|
||||
* nickname
|
||||
* 'fingerprint=nickname' pair
|
||||
* address:port
|
||||
|
||||
The derived 'endpoint_*' attributes are generally more useful.
|
||||
|
||||
The ORCONN event was one of the first Control Protocol V1 events and was
|
||||
introduced in tor version 0.1.1.1-alpha.
|
||||
|
||||
:var str endpoint: relay that the event concerns
|
||||
:var str endpoint_fingerprint: endpoint's finterprint if it was provided
|
||||
:var str endpoint_nickname: endpoint's nickname if it was provided
|
||||
:var str endpoint_address: endpoint's address if it was provided
|
||||
:var int endpoint_port: endpoint's port if it was provided
|
||||
:var stem.ORStatus status: state of the connection
|
||||
:var stem.ORClosureReason reason: reason for the connection to be closed
|
||||
:var int circ_count: number of established and pending circuits
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = ("endpoint", "status")
|
||||
_KEYWORD_ARGS = {
|
||||
"REASON": "reason",
|
||||
"NCIRCS": "circ_count",
|
||||
}
|
||||
|
||||
def _parse(self):
|
||||
self.endpoint_fingerprint = None
|
||||
self.endpoint_nickname = None
|
||||
self.endpoint_address = None
|
||||
self.endpoint_port = None
|
||||
|
||||
try:
|
||||
self.endpoint_fingerprint, self.endpoint_nickname = \
|
||||
stem.control._parse_circ_entry(self.endpoint)
|
||||
except stem.ProtocolError:
|
||||
if not ':' in self.endpoint:
|
||||
raise stem.ProtocolError("ORCONN endpoint is neither a relay nor 'address:port': %s" % self)
|
||||
|
||||
address, port = self.endpoint.split(':', 1)
|
||||
|
||||
if not connection.is_valid_port(port):
|
||||
raise stem.ProtocolError("ORCONN's endpoint location's port is invalid: %s" % self)
|
||||
|
||||
self.endpoint_address = address
|
||||
self.endpoint_port = int(port)
|
||||
|
||||
if self.circ_count is not None:
|
||||
if not self.circ_count.isdigit():
|
||||
raise stem.ProtocolError("ORCONN event got a non-numeric circuit count (%s): %s" % (self.circ_count, self))
|
||||
|
||||
self.circ_count = int(self.circ_count)
|
||||
|
||||
self._log_if_unrecognized('status', stem.ORStatus)
|
||||
self._log_if_unrecognized('reason', stem.ORClosureReason)
|
||||
|
||||
|
||||
class SignalEvent(Event):
|
||||
"""
|
||||
Event that indicates that tor has received and acted upon a signal being sent
|
||||
to the process. As of tor version 0.2.4.6 the only signals conveyed by this
|
||||
event are...
|
||||
|
||||
* RELOAD
|
||||
* DUMP
|
||||
* DEBUG
|
||||
* NEWNYM
|
||||
* CLEARDNSCACHE
|
||||
|
||||
The SIGNAL event was introduced in tor version 0.2.3.1-alpha.
|
||||
|
||||
:var stem.Signal signal: signal that tor received
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = ("signal",)
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_SIGNAL
|
||||
|
||||
def _parse(self):
|
||||
# log if we recieved an unrecognized signal
|
||||
expected_signals = (
|
||||
stem.Signal.RELOAD,
|
||||
stem.Signal.DUMP,
|
||||
stem.Signal.DEBUG,
|
||||
stem.Signal.NEWNYM,
|
||||
stem.Signal.CLEARDNSCACHE,
|
||||
)
|
||||
|
||||
self._log_if_unrecognized('signal', expected_signals)
|
||||
|
||||
|
||||
class StatusEvent(Event):
|
||||
"""
|
||||
Notification of a change in tor's state. These are generally triggered for
|
||||
the same sort of things as log messages of the NOTICE level or higher.
|
||||
However, unlike :class:`~stem.response.events.LogEvent` these contain well
|
||||
formed data.
|
||||
|
||||
The STATUS_GENERAL, STATUS_CLIENT, STATUS_SERVER events were introduced
|
||||
in tor version 0.1.2.3-alpha.
|
||||
|
||||
:var stem.StatusType status_type: category of the status event
|
||||
:var stem.Runlevel runlevel: runlevel of the logged message
|
||||
:var str message: logged message
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = ("runlevel", "action")
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_STATUS
|
||||
|
||||
def _parse(self):
|
||||
if self.type == 'STATUS_GENERAL':
|
||||
self.status_type = stem.StatusType.GENERAL
|
||||
elif self.type == 'STATUS_CLIENT':
|
||||
self.status_type = stem.StatusType.CLIENT
|
||||
elif self.type == 'STATUS_SERVER':
|
||||
self.status_type = stem.StatusType.SERVER
|
||||
else:
|
||||
raise ValueError("BUG: Unrecognized status type (%s), likely an EVENT_TYPE_TO_CLASS addition without revising how 'status_type' is assigned." % self.type)
|
||||
|
||||
self._log_if_unrecognized('runlevel', stem.Runlevel)
|
||||
|
||||
|
||||
class StreamEvent(Event):
|
||||
"""
|
||||
Event that indicates that a stream has changed.
|
||||
|
||||
The STREAM event was one of the first Control Protocol V1 events and was
|
||||
introduced in tor version 0.1.1.1-alpha.
|
||||
|
||||
:var str id: stream identifier
|
||||
:var stem.StreamStatus status: reported status for the stream
|
||||
:var str circ_id: circuit that the stream is attached to
|
||||
:var str target: destination of the stream
|
||||
:var str target_address: destination address (ip, hostname, or '(Tor_internal)')
|
||||
:var int target_port: destination port
|
||||
:var stem.StreamClosureReason reason: reason for the stream to be closed
|
||||
:var stem.StreamClosureReason remote_reason: remote side's reason for the stream to be closed
|
||||
:var stem.StreamSource source: origin of the REMAP request
|
||||
:var str source_addr: requester of the connection
|
||||
:var str source_address: requester address (ip or hostname)
|
||||
:var int source_port: requester port
|
||||
:var stem.StreamPurpose purpose: purpose for the stream
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = ("id", "status", "circ_id", "target")
|
||||
_KEYWORD_ARGS = {
|
||||
"REASON": "reason",
|
||||
"REMOTE_REASON": "remote_reason",
|
||||
"SOURCE": "source",
|
||||
"SOURCE_ADDR": "source_addr",
|
||||
"PURPOSE": "purpose",
|
||||
}
|
||||
|
||||
def _parse(self):
|
||||
if self.target is None:
|
||||
raise stem.ProtocolError("STREAM event didn't have a target: %s" % self)
|
||||
else:
|
||||
if not ':' in self.target:
|
||||
raise stem.ProtocolError("Target location must be of the form 'address:port': %s" % self)
|
||||
|
||||
address, port = self.target.rsplit(':', 1)
|
||||
|
||||
if not connection.is_valid_port(port, allow_zero = True):
|
||||
raise stem.ProtocolError("Target location's port is invalid: %s" % self)
|
||||
|
||||
self.target_address = address
|
||||
self.target_port = int(port)
|
||||
|
||||
if self.source_addr is None:
|
||||
self.source_address = None
|
||||
self.source_port = None
|
||||
else:
|
||||
if not ':' in self.source_addr:
|
||||
raise stem.ProtocolError("Source location must be of the form 'address:port': %s" % self)
|
||||
|
||||
address, port = self.source_addr.split(':', 1)
|
||||
|
||||
if not connection.is_valid_port(port, allow_zero = True):
|
||||
raise stem.ProtocolError("Source location's port is invalid: %s" % self)
|
||||
|
||||
self.source_address = address
|
||||
self.source_port = int(port)
|
||||
|
||||
# spec specifies a circ_id of zero if the stream is unattached
|
||||
|
||||
if self.circ_id == "0":
|
||||
self.circ_id = None
|
||||
|
||||
self._log_if_unrecognized('reason', stem.StreamClosureReason)
|
||||
self._log_if_unrecognized('remote_reason', stem.StreamClosureReason)
|
||||
self._log_if_unrecognized('purpose', stem.StreamPurpose)
|
||||
|
||||
|
||||
class StreamBwEvent(Event):
|
||||
"""
|
||||
Event (emitted approximately every second) with the bytes sent and received
|
||||
by the application since the last such event on this stream.
|
||||
|
||||
The STREAM_BW event was introduced in tor version 0.1.2.8-beta.
|
||||
|
||||
:var str id: stream identifier
|
||||
:var long written: bytes sent by the application
|
||||
:var long read: bytes received by the application
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = ("id", "written", "read")
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_STREAM_BW
|
||||
|
||||
def _parse(self):
|
||||
if not tor_tools.is_valid_stream_id(self.id):
|
||||
raise stem.ProtocolError("Stream IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self))
|
||||
elif not self.written:
|
||||
raise stem.ProtocolError("STREAM_BW event is missing its written value")
|
||||
elif not self.read:
|
||||
raise stem.ProtocolError("STREAM_BW event is missing its read value")
|
||||
elif not self.read.isdigit() or not self.written.isdigit():
|
||||
raise stem.ProtocolError("A STREAM_BW event's bytes sent and received should be a positive numeric value, received: %s" % self)
|
||||
|
||||
self.read = long(self.read)
|
||||
self.written = long(self.written)
|
||||
|
||||
|
||||
class TransportLaunchedEvent(Event):
|
||||
"""
|
||||
Event triggered when a pluggable transport is launched.
|
||||
|
||||
The TRANSPORT_LAUNCHED event was introduced in tor version 0.2.5.0-alpha.
|
||||
|
||||
:var str type: 'server' or 'client'
|
||||
:var str name: name of the pluggable transport
|
||||
:var str address: IPv4 or IPv6 address where the transport is listening for
|
||||
connections
|
||||
:var int port: port where the transport is listening for connections
|
||||
"""
|
||||
|
||||
_POSITIONAL_ARGS = ("type", "name", "address", "port")
|
||||
_VERSION_ADDED = stem.version.Requirement.EVENT_TRANSPORT_LAUNCHED
|
||||
|
||||
def _parse(self):
|
||||
if not self.type in ('server', 'client'):
|
||||
raise stem.ProtocolError("Transport type should either be 'server' or 'client': %s" % self)
|
||||
|
||||
if not connection.is_valid_ipv4_address(self.address) and \
|
||||
not connection.is_valid_ipv6_address(self.address):
|
||||
raise stem.ProtocolError("Transport address isn't a valid IPv4 or IPv6 address: %s" % self)
|
||||
|
||||
if not connection.is_valid_port(self.port):
|
||||
raise stem.ProtocolError("Transport port is invalid: %s" % self)
|
||||
|
||||
self.port = int(self.port)
|
||||
|
||||
EVENT_TYPE_TO_CLASS = {
|
||||
"ADDRMAP": AddrMapEvent,
|
||||
"AUTHDIR_NEWDESCS": AuthDirNewDescEvent,
|
||||
"BUILDTIMEOUT_SET": BuildTimeoutSetEvent,
|
||||
"BW": BandwidthEvent,
|
||||
"CIRC": CircuitEvent,
|
||||
"CIRC_MINOR": CircMinorEvent,
|
||||
"CLIENTS_SEEN": ClientsSeenEvent,
|
||||
"CONF_CHANGED": ConfChangedEvent,
|
||||
"DEBUG": LogEvent,
|
||||
"DESCCHANGED": DescChangedEvent,
|
||||
"ERR": LogEvent,
|
||||
"GUARD": GuardEvent,
|
||||
"INFO": LogEvent,
|
||||
"NEWCONSENSUS": NewConsensusEvent,
|
||||
"NEWDESC": NewDescEvent,
|
||||
"NOTICE": LogEvent,
|
||||
"NS": NetworkStatusEvent,
|
||||
"ORCONN": ORConnEvent,
|
||||
"SIGNAL": SignalEvent,
|
||||
"STATUS_CLIENT": StatusEvent,
|
||||
"STATUS_GENERAL": StatusEvent,
|
||||
"STATUS_SERVER": StatusEvent,
|
||||
"STREAM": StreamEvent,
|
||||
"STREAM_BW": StreamBwEvent,
|
||||
"TRANSPORT_LAUNCHED": TransportLaunchedEvent,
|
||||
"WARN": LogEvent,
|
||||
|
||||
# accounting for a bug in tor 0.2.0.22
|
||||
"STATUS_SEVER": StatusEvent,
|
||||
}
|
55
lib/stem/response/getconf.py
Normal file
55
lib/stem/response/getconf.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Copyright 2012-2013, 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 not key in self.entries:
|
||||
self.entries[key] = []
|
||||
|
||||
if value is not None:
|
||||
self.entries[key].append(value)
|
78
lib/stem/response/getinfo.py
Normal file
78
lib/stem/response/getinfo.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
# Copyright 2012-2013, 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))
|
42
lib/stem/response/mapaddress.py
Normal file
42
lib/stem/response/mapaddress.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Copyright 2012-2013, 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)
|
122
lib/stem/response/protocolinfo.py
Normal file
122
lib/stem/response/protocolinfo.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
# Copyright 2012-2013, 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 not AuthMethod.UNKNOWN in auth_methods:
|
||||
auth_methods.append(AuthMethod.UNKNOWN)
|
||||
|
||||
# parse optional COOKIEFILE mapping (quoted and can have escapes)
|
||||
if line.is_next_mapping("COOKIEFILE", True, True):
|
||||
self.cookie_path = line.pop_mapping(True, True)[1]
|
||||
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)
|
Loading…
Add table
Add a link
Reference in a new issue