run update

This commit is contained in:
j 2018-12-15 01:08:54 +01:00
commit 6806bebb7c
607 changed files with 52543 additions and 31832 deletions

View file

@ -1,4 +1,4 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# Copyright 2012-2018, Damian Johnson and The Tor Project
# See LICENSE for licensing information
"""
@ -16,9 +16,7 @@ Parses replies from the control socket.
|- 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
+- raw_content - unparsed socket data
ControlLine - String subclass with methods for parsing controller responses.
|- remainder - provides the unparsed content
@ -30,6 +28,15 @@ Parses replies from the control socket.
+- pop_mapping - removes and returns the next entry as a KEY=VALUE mapping
"""
import codecs
import io
import re
import threading
import stem.socket
import stem.util
import stem.util.str_tools
__all__ = [
'add_onion',
'events',
@ -43,28 +50,8 @@ __all__ = [
'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):
"""
@ -76,12 +63,13 @@ def convert(response_type, message, **kwargs):
=================== =====
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`
**ADD_ONION** :class:`stem.response.add_onion.AddOnionResponse`
**AUTHCHALLENGE** :class:`stem.response.authchallenge.AuthChallengeResponse`
**EVENT** :class:`stem.response.events.Event` subclass
**GETCONF** :class:`stem.response.getconf.GetConfResponse`
**GETINFO** :class:`stem.response.getinfo.GetInfoResponse`
**MAPADDRESS** :class:`stem.response.mapaddress.MapAddressResponse`
**PROTOCOLINFO** :class:`stem.response.protocolinfo.ProtocolInfoResponse`
**SINGLELINE** :class:`stem.response.SingleLineResponse`
=================== =====
@ -119,11 +107,11 @@ def convert(response_type, message, **kwargs):
'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,
'GETINFO': stem.response.getinfo.GetInfoResponse,
'MAPADDRESS': stem.response.mapaddress.MapAddressResponse,
'SINGLELINE': SingleLineResponse,
'PROTOCOLINFO': stem.response.protocolinfo.ProtocolInfoResponse,
'SINGLELINE': SingleLineResponse,
}
try:
@ -140,23 +128,37 @@ 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.
.. versionchanged:: 1.7.0
Implemented equality and hashing.
"""
@staticmethod
def from_str(content, msg_type = None, **kwargs):
def from_str(content, msg_type = None, normalize = False, **kwargs):
"""
Provides a ControlMessage for the given content.
.. versionadded:: 1.1.0
.. versionchanged:: 1.6.0
Added the normalize argument.
:param str content: message to construct the message from
:param str msg_type: type of tor reply to parse the content as
:param bool normalize: ensures expected carriage return and ending newline
are present
: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 normalize:
if not content.endswith('\n'):
content += '\n'
content = re.sub('([\r]?)\n', '\r\n', content)
msg = stem.socket.recv_message(io.BytesIO(stem.util.str_tools._to_bytes(content)))
if msg_type is not None:
convert(msg_type, msg, **kwargs)
@ -169,6 +171,8 @@ class ControlMessage(object):
self._parsed_content = parsed_content
self._raw_content = raw_content
self._str = None
self._hash = stem.util._hash_attr(self, '_raw_content')
def is_ok(self):
"""
@ -245,7 +249,10 @@ class ControlMessage(object):
formatting.
"""
return '\n'.join(list(self))
if self._str is None:
self._str = '\n'.join(list(self))
return self._str
def __iter__(self):
"""
@ -295,6 +302,15 @@ class ControlMessage(object):
return ControlLine(content)
def __hash__(self):
return self._hash
def __eq__(self, other):
return hash(self) == hash(other) if isinstance(other, ControlMessage) else False
def __ne__(self, other):
return not self == other
class ControlLine(str):
"""
@ -336,7 +352,7 @@ class ControlLine(str):
"""
Checks if our next entry is a quoted value or not.
:param bool escaped: unescapes the CONTROL_ESCAPES escape sequences
:param bool escaped: unescapes the string
:returns: **True** if the next entry can be parsed as a quoted value, **False** otherwise
"""
@ -350,7 +366,7 @@ class ControlLine(str):
: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
:param bool escaped: unescapes the string
:returns: **True** if the next entry can be parsed as a key=value mapping,
**False** otherwise
@ -408,7 +424,7 @@ class ControlLine(str):
"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
:param bool escaped: unescapes the string
:returns: **str** of the next space separated entry
@ -418,17 +434,21 @@ class ControlLine(str):
"""
with self._remainder_lock:
next_entry, remainder = _parse_entry(self._remainder, quoted, escaped)
next_entry, remainder = _parse_entry(self._remainder, quoted, escaped, False)
self._remainder = remainder
return next_entry
def pop_mapping(self, quoted = False, escaped = False):
def pop_mapping(self, quoted = False, escaped = False, get_bytes = False):
"""
Parses the next space separated entry as a KEY=VALUE mapping, removing it
and the space from our remaining content.
.. versionchanged:: 1.6.0
Added the get_bytes argument.
:param bool quoted: parses the value as being quoted, removing the quotes
:param bool escaped: unescapes the CONTROL_ESCAPES escape sequences
:param bool escaped: unescapes the string
:param bool get_bytes: provides **bytes** for the **value** rather than a **str**
:returns: **tuple** of the form (key, value)
@ -450,18 +470,18 @@ class ControlLine(str):
key = key_match.groups()[0]
remainder = self._remainder[key_match.end():]
next_entry, remainder = _parse_entry(remainder, quoted, escaped)
next_entry, remainder = _parse_entry(remainder, quoted, escaped, get_bytes)
self._remainder = remainder
return (key, next_entry)
def _parse_entry(line, quoted, escaped):
def _parse_entry(line, quoted, escaped, get_bytes):
"""
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
:param bool escaped: unescapes the string
:returns: **tuple** of the form (entry, remainder)
@ -491,7 +511,26 @@ def _parse_entry(line, quoted, escaped):
next_entry, remainder = remainder, ''
if escaped:
next_entry = _unescape(next_entry)
# Tor does escaping in its 'esc_for_log' function of '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
#
# Ideally we'd use "next_entry.decode('string_escape')" but it was removed
# in python 3.x and 'unicode_escape' isn't quite the same...
#
# https://stackoverflow.com/questions/14820429/how-do-i-decodestring-escape-in-python3
next_entry = codecs.escape_decode(next_entry)[0]
if stem.prereq.is_python_3() and not get_bytes:
next_entry = stem.util.str_tools._to_unicode(next_entry) # normalize back to str
if get_bytes:
next_entry = stem.util.str_tools._to_bytes(next_entry)
return (next_entry, remainder.lstrip())
@ -501,7 +540,7 @@ 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
:param bool escaped: unescapes the string
:returns: **tuple** of two ints, indices being -1 if a quote doesn't exist
"""
@ -522,34 +561,6 @@ def _get_quote_indices(line, escaped):
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

View file

@ -1,4 +1,4 @@
# Copyright 2015, Damian Johnson and The Tor Project
# Copyright 2015-2018, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import stem.response
@ -12,17 +12,20 @@ class AddOnionResponse(stem.response.ControlMessage):
: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)
:var dict client_auth: newly generated client credentials the service accepts
"""
def _parse_message(self):
# Example:
# 250-ServiceID=gfzprpioee3hoppz
# 250-PrivateKey=RSA1024:MIICXgIBAAKBgQDZvYVxv...
# 250-ClientAuth=bob:l4BT016McqV2Oail+Bwe6w
# 250 OK
self.service_id = None
self.private_key = None
self.private_key_type = None
self.client_auth = {}
if not self.is_ok():
raise stem.ProtocolError("ADD_ONION response didn't have an OK status: %s" % self)
@ -41,3 +44,9 @@ class AddOnionResponse(stem.response.ControlMessage):
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)
elif key == 'ClientAuth':
if ':' not in value:
raise stem.ProtocolError("ADD_ONION ClientAuth lines should be of the form 'ClientAuth=[username]:[credential]: %s" % self)
username, credential = value.split(':', 1)
self.client_auth[username] = credential

View file

@ -1,4 +1,4 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# Copyright 2012-2018, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import binascii
@ -41,7 +41,7 @@ class AuthChallengeResponse(stem.response.ControlMessage):
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))
self.server_hash = binascii.unhexlify(stem.util.str_tools._to_bytes(value))
else:
raise stem.ProtocolError('Missing SERVERHASH mapping: %s' % line)
@ -51,6 +51,6 @@ class AuthChallengeResponse(stem.response.ControlMessage):
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))
self.server_nonce = binascii.unhexlify(stem.util.str_tools._to_bytes(value))
else:
raise stem.ProtocolError('Missing SERVERNONCE mapping: %s' % line)

View file

@ -1,4 +1,4 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# Copyright 2012-2018, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import io
@ -8,10 +8,11 @@ import time
import stem
import stem.control
import stem.descriptor.router_status_entry
import stem.prereq
import stem.response
import stem.util
import stem.version
from stem import str_type, int_type
from stem.util import connection, log, str_tools, tor_tools
# Matches keyword=value arguments. This can't be a simple "(.*)=(.*)" pattern
@ -21,6 +22,11 @@ from stem.util import connection, log, str_tools, tor_tools
KW_ARG = re.compile('^(.*) ([A-Za-z0-9_]+)=(\S*)$')
QUOTED_KW_ARG = re.compile('^(.*) ([A-Za-z0-9_]+)="(.*)"$')
CELL_TYPE = re.compile('^[a-z0-9_]+$')
PARSE_NEWCONSENSUS_EVENTS = True
# TODO: We can remove the following when we drop python2.6 support.
INT_TYPE = int if stem.prereq.is_python_3() else long
class Event(stem.response.ControlMessage):
@ -65,6 +71,9 @@ class Event(stem.response.ControlMessage):
self._parse()
def __hash__(self):
return stem.util._hash_attr(self, 'arrived_at', parent = stem.response.ControlMessage, cache = True)
def _parse_standard_attr(self):
"""
Most events are of the form...
@ -126,6 +135,25 @@ class Event(stem.response.ControlMessage):
for controller_attr_name, attr_name in self._KEYWORD_ARGS.items():
setattr(self, attr_name, self.keyword_args.get(controller_attr_name))
def _iso_timestamp(self, timestamp):
"""
Parses an iso timestamp (ISOTime2Frac in the control-spec).
:param str timestamp: timestamp to parse
:returns: **datetime** with the parsed timestamp
:raises: :class:`stem.ProtocolError` if timestamp is malformed
"""
if timestamp is None:
return None
try:
return str_tools._parse_iso_timestamp(timestamp)
except ValueError as exc:
raise stem.ProtocolError('Unable to parse timestamp (%s): %s' % (exc, self))
# method overwritten by our subclasses for special handling that they do
def _parse(self):
pass
@ -142,7 +170,7 @@ class Event(stem.response.ControlMessage):
attr_values = getattr(self, attr)
if attr_values:
if isinstance(attr_values, (bytes, str_type)):
if stem.util._is_str(attr_values):
attr_values = [attr_values]
for value in attr_values:
@ -163,7 +191,7 @@ class AddrMapEvent(Event):
Added the cached attribute.
:var str hostname: address being resolved
:var str destination: destionation of the resolution, this is usually an ip,
:var str destination: destination 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
@ -212,7 +240,11 @@ class AuthDirNewDescEvent(Event):
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.
The AUTHDIR_NEWDESCS event was introduced in tor version 0.1.1.10-alpha and
removed in 0.3.2.1-alpha. (:spec:`6e887ba`)
.. deprecated:: 1.6.0
Tor dropped this event as of version 0.3.2.1. (:spec:`6e887ba`)
:var stem.AuthDescriptorAction action: what is being done with the descriptor
:var str message: explanation of why we chose this action
@ -245,8 +277,8 @@ class BandwidthEvent(Event):
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
:var int read: bytes received by tor that second
:var int written: bytes sent by tor that second
"""
_POSITIONAL_ARGS = ('read', 'written')
@ -259,8 +291,8 @@ class BandwidthEvent(Event):
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 = int_type(self.read)
self.written = int_type(self.written)
self.read = INT_TYPE(self.read)
self.written = INT_TYPE(self.written)
class BuildTimeoutSetEvent(Event):
@ -365,16 +397,11 @@ class CircuitEvent(Event):
def _parse(self):
self.path = tuple(stem.control._parse_circ_path(self.path))
self.created = self._iso_timestamp(self.created)
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))
@ -386,35 +413,15 @@ class CircuitEvent(Event):
self._log_if_unrecognized('remote_reason', stem.CircClosureReason)
def _compare(self, other, method):
# sorting circuit events by their identifier
if not isinstance(other, CircuitEvent):
return False
for attr in ('id', 'status', 'path', 'build_flags', 'purpose', 'hs_state', 'rend_query', 'created', 'reason', 'remote_reason', 'socks_username', 'socks_port'):
my_attr = getattr(self, attr)
other_attr = getattr(other, attr)
my_id = getattr(self, 'id')
their_id = getattr(other, 'id')
# Our id attribute is technically a string, but Tor conventionally uses
# ints. Attempt to handle as ints if that's the case so we get numeric
# ordering.
if attr == 'id' and my_attr and other_attr:
if my_attr.isdigit() and other_attr.isdigit():
my_attr = int(my_attr)
other_attr = int(other_attr)
if my_attr is None:
my_attr = ''
if other_attr is None:
other_attr = ''
if my_attr != other_attr:
return method(my_attr, other_attr)
return True
def __eq__(self, other):
return self._compare(other, lambda s, o: s == o)
return method(my_id, their_id) if my_id != their_id else method(hash(self), hash(other))
def __gt__(self, other):
return self._compare(other, lambda s, o: s > o)
@ -458,16 +465,11 @@ class CircMinorEvent(Event):
def _parse(self):
self.path = tuple(stem.control._parse_circ_path(self.path))
self.created = self._iso_timestamp(self.created)
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))
@ -545,15 +547,26 @@ class ConfChangedEvent(Event):
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)
.. deprecated:: 1.7.0
Deprecated the *config* attribute. Some tor configuration options (like
ExitPolicy) can have multiple values, so a simple 'str => str' mapping
meant that we only provided the last.
.. versionchanged:: 1.7.0
Added the changed and unset attributes.
:var dict changed: mapping of configuration options to a list of their new
values
:var list unset: configuration options that have been unset
"""
_SKIP_PARSING = True
_VERSION_ADDED = stem.version.Requirement.EVENT_CONF_CHANGED
def _parse(self):
self.config = {}
self.changed = {}
self.unset = []
self.config = {} # TODO: remove in stem 2.0
# Skip first and last line since they're the header and footer. For
# instance...
@ -567,8 +580,10 @@ class ConfChangedEvent(Event):
for line in str(self).splitlines()[1:-1]:
if '=' in line:
key, value = line.split('=', 1)
self.changed.setdefault(key, []).append(value)
else:
key, value = line, None
self.unset.append(key)
self.config[key] = value
@ -630,6 +645,12 @@ class HSDescEvent(Event):
.. versionchanged:: 1.3.0
Added the reason attribute.
.. versionchanged:: 1.5.0
Added the replica attribute.
.. versionchanged:: 1.7.0
Added the index attribute.
:var stem.HSDescAction action: what is happening with the descriptor
:var str address: hidden service address
:var stem.HSAuth authentication: service's authentication method
@ -638,21 +659,30 @@ class HSDescEvent(Event):
:var str directory_nickname: hidden service directory's nickname if it was provided
:var str descriptor_id: descriptor identifier
:var stem.HSDescReason reason: reason the descriptor failed to be fetched
:var int replica: replica number the descriptor involves
:var str index: computed index of the HSDir the descriptor was uploaded to or fetched from
"""
_VERSION_ADDED = stem.version.Requirement.EVENT_HS_DESC
_POSITIONAL_ARGS = ('action', 'address', 'authentication', 'directory', 'descriptor_id')
_KEYWORD_ARGS = {'REASON': 'reason'}
_KEYWORD_ARGS = {'REASON': 'reason', 'REPLICA': 'replica', 'HSDIR_INDEX': 'index'}
def _parse(self):
self.directory_fingerprint = None
self.directory_nickname = None
try:
self.directory_fingerprint, self.directory_nickname = \
stem.control._parse_circ_entry(self.directory)
except stem.ProtocolError:
raise stem.ProtocolError("HS_DESC's directory doesn't match a ServerSpec: %s" % self)
if self.directory != 'UNKNOWN':
try:
self.directory_fingerprint, self.directory_nickname = \
stem.control._parse_circ_entry(self.directory)
except stem.ProtocolError:
raise stem.ProtocolError("HS_DESC's directory doesn't match a ServerSpec: %s" % self)
if self.replica is not None:
if not self.replica.isdigit():
raise stem.ProtocolError('HS_DESC event got a non-numeric replica count (%s): %s' % (self.replica, self))
self.replica = int(self.replica)
self._log_if_unrecognized('action', stem.HSDescAction)
self._log_if_unrecognized('authentication', stem.HSAuth)
@ -744,11 +774,27 @@ class NetworkStatusEvent(Event):
self.desc = list(stem.descriptor.router_status_entry._parse_file(
io.BytesIO(str_tools._to_bytes(content)),
True,
False,
entry_class = stem.descriptor.router_status_entry.RouterStatusEntryV3,
))
class NetworkLivenessEvent(Event):
"""
Event for when the network becomes reachable or unreachable.
The NETWORK_LIVENESS event was introduced in tor version 0.2.7.2-alpha.
.. versionadded:: 1.5.0
:var str status: status of the network ('UP', 'DOWN', or possibly other
statuses in the future)
"""
_VERSION_ADDED = stem.version.Requirement.EVENT_NETWORK_LIVENESS
_POSITIONAL_ARGS = ('status',)
class NewConsensusEvent(Event):
"""
Event for when we have a new consensus. This is similar to
@ -758,6 +804,19 @@ class NewConsensusEvent(Event):
The NEWCONSENSUS event was introduced in tor version 0.2.1.13-alpha.
.. versionchanged:: 1.6.0
Added the consensus_content attribute.
.. deprecated:: 1.6.0
In Stem 2.0 we'll remove the desc attribute, so this event only provides
the unparsed consensus. Callers can then parse it if they'd like. To drop
parsing before then you can set...
::
stem.response.events.PARSE_NEWCONSENSUS_EVENTS = False
:var str consensus_content: consensus content
:var list desc: :class:`~stem.descriptor.router_status_entry.RouterStatusEntryV3` for the changed descriptors
"""
@ -765,16 +824,19 @@ class NewConsensusEvent(Event):
_VERSION_ADDED = stem.version.Requirement.EVENT_NEWCONSENSUS
def _parse(self):
content = str(self).lstrip('NEWCONSENSUS\n').rstrip('\nOK')
self.consensus_content = str(self).lstrip('NEWCONSENSUS\n').rstrip('\nOK')
# TODO: For stem 2.0.0 consider changing 'desc' to 'descriptors' to match
# our other events.
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,
))
if PARSE_NEWCONSENSUS_EVENTS:
self.desc = list(stem.descriptor.router_status_entry._parse_file(
io.BytesIO(str_tools._to_bytes(self.consensus_content)),
False,
entry_class = stem.descriptor.router_status_entry.RouterStatusEntryV3,
))
else:
self.desc = None
class NewDescEvent(Event):
@ -846,7 +908,7 @@ class ORConnEvent(Event):
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)
address, port = self.endpoint.rsplit(':', 1)
if not connection.is_valid_port(port):
raise stem.ProtocolError("ORCONN's endpoint location's port is invalid: %s" % self)
@ -993,7 +1055,7 @@ class StreamEvent(Event):
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)
address, port = self.source_addr.rsplit(':', 1)
if not connection.is_valid_port(port, allow_zero = True):
raise stem.ProtocolError("Source location's port is invalid: %s" % self)
@ -1018,12 +1080,16 @@ class StreamBwEvent(Event):
The STREAM_BW event was introduced in tor version 0.1.2.8-beta.
.. versionchanged:: 1.6.0
Added the time attribute.
:var str id: stream identifier
:var long written: bytes sent by the application
:var long read: bytes received by the application
:var int written: bytes sent by the application
:var int read: bytes received by the application
:var datetime time: time when the measurement was recorded
"""
_POSITIONAL_ARGS = ('id', 'written', 'read')
_POSITIONAL_ARGS = ('id', 'written', 'read', 'time')
_VERSION_ADDED = stem.version.Requirement.EVENT_STREAM_BW
def _parse(self):
@ -1036,8 +1102,9 @@ class StreamBwEvent(Event):
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 = int_type(self.read)
self.written = int_type(self.written)
self.read = INT_TYPE(self.read)
self.written = INT_TYPE(self.written)
self.time = self._iso_timestamp(self.time)
class TransportLaunchedEvent(Event):
@ -1081,15 +1148,19 @@ class ConnectionBandwidthEvent(Event):
.. versionadded:: 1.2.0
.. versionchanged:: 1.6.0
Renamed 'type' attribute to 'conn_type' so it wouldn't be override parent
class attribute with the same name.
:var str id: connection identifier
:var stem.ConnectionType type: connection type
:var long read: bytes received by tor that second
:var long written: bytes sent by tor that second
:var stem.ConnectionType conn_type: connection type
:var int read: bytes received by tor that second
:var int written: bytes sent by tor that second
"""
_KEYWORD_ARGS = {
'ID': 'id',
'TYPE': 'type',
'TYPE': 'conn_type',
'READ': 'read',
'WRITTEN': 'written',
}
@ -1099,8 +1170,8 @@ class ConnectionBandwidthEvent(Event):
def _parse(self):
if not self.id:
raise stem.ProtocolError('CONN_BW event is missing its id')
elif not self.type:
raise stem.ProtocolError('CONN_BW event is missing its type')
elif not self.conn_type:
raise stem.ProtocolError('CONN_BW event is missing its connection type')
elif not self.read:
raise stem.ProtocolError('CONN_BW event is missing its read value')
elif not self.written:
@ -1110,10 +1181,10 @@ class ConnectionBandwidthEvent(Event):
elif not tor_tools.is_valid_connection_id(self.id):
raise stem.ProtocolError("Connection IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self))
self.read = int_type(self.read)
self.written = int_type(self.written)
self.read = INT_TYPE(self.read)
self.written = INT_TYPE(self.written)
self._log_if_unrecognized('type', stem.ConnectionType)
self._log_if_unrecognized('conn_type', stem.ConnectionType)
class CircuitBandwidthEvent(Event):
@ -1125,15 +1196,32 @@ class CircuitBandwidthEvent(Event):
.. versionadded:: 1.2.0
.. versionchanged:: 1.6.0
Added the time attribute.
.. versionchanged:: 1.7.0
Added the delivered_read, delivered_written, overhead_read, and
overhead_written attributes.
:var str id: circuit identifier
:var long read: bytes received by tor that second
:var long written: bytes sent by tor that second
:var int read: bytes received by tor that second
:var int written: bytes sent by tor that second
:var int delivered_read: user payload received by tor that second
:var int delivered_written: user payload sent by tor that second
:var int overhead_read: padding so read cells will have a fixed length
:var int overhead_written: padding so written cells will have a fixed length
:var datetime time: time when the measurement was recorded
"""
_KEYWORD_ARGS = {
'ID': 'id',
'READ': 'read',
'WRITTEN': 'written',
'DELIVERED_READ': 'delivered_read',
'DELIVERED_WRITTEN': 'delivered_written',
'OVERHEAD_READ': 'overhead_read',
'OVERHEAD_WRITTEN': 'overhead_written',
'TIME': 'time',
}
_VERSION_ADDED = stem.version.Requirement.EVENT_CIRC_BW
@ -1145,13 +1233,28 @@ class CircuitBandwidthEvent(Event):
raise stem.ProtocolError('CIRC_BW event is missing its read value')
elif not self.written:
raise stem.ProtocolError('CIRC_BW event is missing its written value')
elif not self.read.isdigit() or not self.written.isdigit():
raise stem.ProtocolError("A CIRC_BW event's bytes sent and received should be a positive numeric value, received: %s" % self)
elif not self.read.isdigit():
raise stem.ProtocolError("A CIRC_BW event's bytes received should be a positive numeric value, received: %s" % self)
elif not self.written.isdigit():
raise stem.ProtocolError("A CIRC_BW event's bytes sent should be a positive numeric value, received: %s" % self)
elif self.delivered_read and not self.delivered_read.isdigit():
raise stem.ProtocolError("A CIRC_BW event's delivered bytes received should be a positive numeric value, received: %s" % self)
elif self.delivered_written and not self.delivered_written.isdigit():
raise stem.ProtocolError("A CIRC_BW event's delivered bytes sent should be a positive numeric value, received: %s" % self)
elif self.overhead_read and not self.overhead_read.isdigit():
raise stem.ProtocolError("A CIRC_BW event's overhead bytes received should be a positive numeric value, received: %s" % self)
elif self.overhead_written and not self.overhead_written.isdigit():
raise stem.ProtocolError("A CIRC_BW event's overhead bytes sent should be a positive numeric value, received: %s" % self)
elif 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.read = int_type(self.read)
self.written = int_type(self.written)
self.time = self._iso_timestamp(self.time)
for attr in ('read', 'written', 'delivered_read', 'delivered_written', 'overhead_read', 'overhead_written'):
value = getattr(self, attr)
if value:
setattr(self, attr, INT_TYPE(value))
class CellStatsEvent(Event):
@ -1280,7 +1383,7 @@ def _parse_cell_type_mapping(mapping):
if ':' not in entry:
raise stem.ProtocolError("Mappings are expected to be of the form 'key:value', got '%s': %s" % (entry, mapping))
key, value = entry.split(':', 1)
key, value = entry.rsplit(':', 1)
if not CELL_TYPE.match(key):
raise stem.ProtocolError("Key had invalid characters, got '%s': %s" % (key, mapping))
@ -1311,6 +1414,7 @@ EVENT_TYPE_TO_CLASS = {
'HS_DESC': HSDescEvent,
'HS_DESC_CONTENT': HSDescContentEvent,
'INFO': LogEvent,
'NETWORK_LIVENESS': NetworkLivenessEvent,
'NEWCONSENSUS': NewConsensusEvent,
'NEWDESC': NewDescEvent,
'NOTICE': LogEvent,

View file

@ -1,4 +1,4 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# Copyright 2012-2018, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import stem.response
@ -48,6 +48,14 @@ class GetConfResponse(stem.response.ControlMessage):
else:
key, value = (line.pop(), None)
# Tor's CommaList and RouterList have a bug where they map to an empty
# string when undefined rather than None...
#
# https://trac.torproject.org/projects/tor/ticket/18263
if value == '':
value = None
if key not in self.entries:
self.entries[key] = []

View file

@ -1,4 +1,4 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# Copyright 2012-2018, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import stem.response
@ -30,12 +30,20 @@ class GetInfoResponse(stem.response.ControlMessage):
if not self.is_ok() or not remaining_lines.pop() == b'OK':
unrecognized_keywords = []
error_code, error_msg = None, None
for code, _, line in self.content():
if code != '250':
error_code = code
error_msg = line
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)
elif error_code:
raise stem.OperationFailed(error_code, error_msg)
else:
raise stem.ProtocolError("GETINFO response didn't have an OK status:\n%s" % self)

View file

@ -1,4 +1,4 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# Copyright 2012-2018, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import stem.response

View file

@ -1,9 +1,13 @@
# Copyright 2012-2015, Damian Johnson and The Tor Project
# Copyright 2012-2018, Damian Johnson and The Tor Project
# See LICENSE for licensing information
import sys
import stem.prereq
import stem.response
import stem.socket
import stem.version
import stem.util.str_tools
from stem.connection import AuthMethod
from stem.util import log
@ -101,8 +105,12 @@ class ProtocolInfoResponse(stem.response.ControlMessage):
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]
self.cookie_path = line.pop_mapping(True, True, get_bytes = True)[1].decode(sys.getfilesystemencoding())
if stem.prereq.is_python_3():
self.cookie_path = stem.util.str_tools._to_unicode(self.cookie_path) # normalize back to str
elif line_type == 'VERSION':
# Line format:
# VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF