run update
This commit is contained in:
parent
11af4540c5
commit
6806bebb7c
607 changed files with 52543 additions and 31832 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue