[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Move Relay class into stem.client
commit 9b3a868a80e91527d9cdf276b3e27eef1bf34b02
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
Date: Sun Feb 4 13:09:28 2018 -0800
Move Relay class into stem.client
On reflection I'd really prefer to keep all these modules colocated in this
module, much as stem.descriptor covers all things descriptor related.
---
stem/__init__.py | 1 -
stem/client/__init__.py | 594 ++++++----------------------------------
stem/client/cell.py | 36 ++-
stem/client/datatype.py | 533 +++++++++++++++++++++++++++++++++++
stem/relay.py | 137 ---------
test/integ/client/__init__.py | 2 +-
test/integ/client/connection.py | 2 +-
test/settings.cfg | 2 +-
test/unit/client/address.py | 2 +-
test/unit/client/cell.py | 2 +-
test/unit/client/certificate.py | 2 +-
test/unit/client/kdf.py | 6 +-
test/unit/client/size.py | 2 +-
13 files changed, 660 insertions(+), 661 deletions(-)
diff --git a/stem/__init__.py b/stem/__init__.py
index 2d2e1c6d..83a48903 100644
--- a/stem/__init__.py
+++ b/stem/__init__.py
@@ -494,7 +494,6 @@ __all__ = [
'exit_policy',
'prereq',
'process',
- 'relay',
'socket',
'version',
'ControllerError',
diff --git a/stem/client/__init__.py b/stem/client/__init__.py
index 9f4217c5..85473685 100644
--- a/stem/client/__init__.py
+++ b/stem/client/__init__.py
@@ -2,556 +2,142 @@
# See LICENSE for licensing information
"""
-Support for `Tor's ORPort protocol
-<https://gitweb.torproject.org/torspec.git/tree/tor-spec.txt>`_.
-
-**This module only consists of low level components, and is not intended for
-users.** See our :class:`~stem.relay.Relay` the API you probably want.
+Interaction with a Tor relay's ORPort. :class:`~stem.client.Relay` is
+a wrapper for :class:`~stem.socket.RelaySocket`, much the same way as
+:class:`~stem.control.Controller` provides higher level functions for
+:class:`~stem.socket.ControlSocket`.
.. versionadded:: 1.7.0
::
- split - splits bytes into substrings
-
- Field - Packable and unpackable datatype.
- |- Size - Field of a static size.
- |- Address - Relay address.
- |- Certificate - Relay certificate.
+ Relay - Connection with a tor relay's ORPort.
+ | +- connect - Establishes a connection with a relay.
|
- |- pack - encodes content
- |- unpack - decodes content
- +- pop - decodes content with remainder
-
- KDF - KDF-TOR derivatived attributes
- +- from_value - parses key material
-
-.. data:: AddrType (enum)
-
- Form an address takes.
-
- ===================== ===========
- AddressType Description
- ===================== ===========
- **HOSTNAME** relay hostname
- **IPv4** IPv4 address
- **IPv6** IPv6 address
- **ERROR_TRANSIENT** temporarily error retrieving address
- **ERROR_PERMANENT** permanent error retrieving address
- **UNKNOWN** unrecognized address type
- ===================== ===========
-
-.. data:: RelayCommand (enum)
-
- Command concerning streams and circuits we've established with a relay.
- Commands have two characteristics...
-
- * **forward/backward**: **forward** commands are issued from the orgin,
- whereas **backward** come from the relay
-
- * **stream/circuit**: **steam** commands concern an individual steam, whereas
- **circuit** concern the entire circuit we've established with a relay
-
- ===================== ===========
- RelayCommand Description
- ===================== ===========
- **BEGIN** begin a stream (**forward**, **stream**)
- **DATA** transmit data (**forward/backward**, **stream**)
- **END** end a stream (**forward/backward**, **stream**)
- **CONNECTED** BEGIN reply (**backward**, **stream**)
- **SENDME** ready to accept more cells (**forward/backward**, **stream/circuit**)
- **EXTEND** extend the circuit through another relay (**forward**, **circuit**)
- **EXTENDED** EXTEND reply (**backward**, **circuit**)
- **TRUNCATE** remove last circuit hop (**forward**, **circuit**)
- **TRUNCATED** TRUNCATE reply (**backward**, **circuit**)
- **DROP** ignorable no-op (**forward/backward**, **circuit**)
- **RESOLVE** request DNS resolution (**forward**, **stream**)
- **RESOLVED** RESOLVE reply (**backward**, **stream**)
- **BEGIN_DIR** request descriptor (**forward**, **steam**)
- **EXTEND2** ntor EXTEND request (**forward**, **circuit**)
- **EXTENDED2** EXTEND2 reply (**backward**, **circuit**)
- **UNKNOWN** unrecognized command
- ===================== ===========
-
-.. data:: CertType (enum)
-
- Relay certificate type.
-
- ===================== ===========
- CertType Description
- ===================== ===========
- **LINK** link key certificate certified by RSA1024 identity
- **IDENTITY** RSA1024 Identity certificate
- **AUTHENTICATE** RSA1024 AUTHENTICATE cell link certificate
- **UNKNOWN** unrecognized certificate type
- ===================== ===========
-
-.. data:: CloseReason (enum)
-
- Reason a relay is closed.
-
- ===================== ===========
- CloseReason Description
- ===================== ===========
- **NONE** no reason given
- **PROTOCOL** tor protocol violation
- **INTERNAL** internal error
- **REQUESTED** client sent a TRUNCATE command
- **HIBERNATING** relay suspended, trying to save bandwidth
- **RESOURCELIMIT** out of memory, sockets, or circuit IDs
- **CONNECTFAILED** unable to reach relay
- **OR_IDENTITY** connected, but its OR identity was not as expected
- **OR_CONN_CLOSED** connection that was carrying this circuit died
- **FINISHED** circuit has expired for being dirty or old
- **TIMEOUT** circuit construction took too long
- **DESTROYED** circuit was destroyed without a client TRUNCATE
- **NOSUCHSERVICE** request was for an unknown hidden service
- **UNKNOWN** unrecognized reason
- ===================== ===========
+ |- is_alive - reports if our connection is open or closed
+ |- connection_time - time when we last connected or disconnected
+ +- close - shuts down our connection
"""
-import collections
-import hashlib
-import io
-import struct
-
-import stem.prereq
+import stem
+import stem.client.cell
+import stem.socket
import stem.util.connection
-import stem.util.enum
-
-from stem.util import _hash_attr
-ZERO = '\x00'
-HASH_LEN = 20
-KEY_LEN = 16
+from stem.client.datatype import AddrType, Address
__all__ = [
'cell',
+ 'datatype',
]
+DEFAULT_LINK_PROTOCOLS = (3, 4, 5)
-class _IntegerEnum(stem.util.enum.Enum):
- """
- Integer backed enumeration. Enumerations of this type always have an implicit
- **UNKNOWN** value for integer values that lack a mapping.
- """
-
- def __init__(self, *args):
- self._enum_to_int = {}
- self._int_to_enum = {}
- parent_args = []
-
- for entry in args:
- if len(entry) == 2:
- enum, int_val = entry
- str_val = enum
- elif len(entry) == 3:
- enum, str_val, int_val = entry
- else:
- raise ValueError('IntegerEnums can only be constructed with two or three value tuples: %s' % repr(entry))
-
- self._enum_to_int[str_val] = int_val
- self._int_to_enum[int_val] = str_val
- parent_args.append((enum, str_val))
-
- parent_args.append(('UNKNOWN', 'UNKNOWN'))
- super(_IntegerEnum, self).__init__(*parent_args)
-
- def get(self, val):
- """
- Privides the (enum, int_value) tuple for a given value.
- """
- if isinstance(val, int):
- return self._int_to_enum.get(val, self.UNKNOWN), val
- elif val in self:
- return val, self._enum_to_int.get(val, val)
- else:
- raise ValueError("Invalid enumeration '%s', options are %s" % (val, ', '.join(self)))
-
-
-AddrType = _IntegerEnum(
- ('HOSTNAME', 0),
- ('IPv4', 4),
- ('IPv6', 6),
- ('ERROR_TRANSIENT', 16),
- ('ERROR_PERMANENT', 17),
-)
-
-RelayCommand = _IntegerEnum(
- ('BEGIN', 'RELAY_BEGIN', 1),
- ('DATA', 'RELAY_DATA', 2),
- ('END', 'RELAY_END', 3),
- ('CONNECTED', 'RELAY_CONNECTED', 4),
- ('SENDME', 'RELAY_SENDME', 5),
- ('EXTEND', 'RELAY_EXTEND', 6),
- ('EXTENDED', 'RELAY_EXTENDED', 7),
- ('TRUNCATE', 'RELAY_TRUNCATE', 8),
- ('TRUNCATED', 'RELAY_TRUNCATED', 9),
- ('DROP', 'RELAY_DROP', 10),
- ('RESOLVE', 'RELAY_RESOLVE', 11),
- ('RESOLVED', 'RELAY_RESOLVED', 12),
- ('BEGIN_DIR', 'RELAY_BEGIN_DIR', 13),
- ('EXTEND2', 'RELAY_EXTEND2', 14),
- ('EXTENDED2', 'RELAY_EXTENDED2', 15),
-)
-
-CertType = _IntegerEnum(
- ('LINK', 1),
- ('IDENTITY', 2),
- ('AUTHENTICATE', 3),
-)
-
-CloseReason = _IntegerEnum(
- ('NONE', 0),
- ('PROTOCOL', 1),
- ('INTERNAL', 2),
- ('REQUESTED', 3),
- ('HIBERNATING', 4),
- ('RESOURCELIMIT', 5),
- ('CONNECTFAILED', 6),
- ('OR_IDENTITY', 7),
- ('OR_CONN_CLOSED', 8),
- ('FINISHED', 9),
- ('TIMEOUT', 10),
- ('DESTROYED', 11),
- ('NOSUCHSERVICE', 12),
-)
-
-STREAM_ID_REQUIRED = (
- RelayCommand.BEGIN,
- RelayCommand.DATA,
- RelayCommand.END,
- RelayCommand.CONNECTED,
- RelayCommand.RESOLVE,
- RelayCommand.RESOLVED,
- RelayCommand.BEGIN_DIR,
-)
-
-STREAM_ID_DISALLOWED = (
- RelayCommand.EXTEND,
- RelayCommand.EXTENDED,
- RelayCommand.TRUNCATE,
- RelayCommand.TRUNCATED,
- RelayCommand.DROP,
- RelayCommand.EXTEND2,
- RelayCommand.EXTENDED2,
-)
-
-
-def split(content, size):
- """
- Simple split of bytes into two substrings.
-
- :param bytes content: string to split
- :param int size: index to split the string on
-
- :returns: two value tuple with the split bytes
+class Relay(object):
"""
+ Connection with a Tor relay's ORPort.
- return content[:size], content[size:]
-
-
-class Field(object):
+ :var int link_protocol: link protocol version we established
"""
- Packable and unpackable datatype.
- """
-
- def pack(self):
- """
- Encodes field into bytes.
-
- :returns: **bytes** that can be communicated over Tor's ORPort
-
- :raises: **ValueError** if incorrect type or size
- """
- raise NotImplementedError('Not yet available')
-
- @classmethod
- def unpack(cls, packed):
- """
- Decodes bytes into a field of this type.
-
- :param bytes packed: content to decode
-
- :returns: instance of this class
-
- :raises: **ValueError** if packed data is malformed
- """
-
- unpacked, remainder = cls.pop(packed)
-
- if remainder:
- raise ValueError('%s is the wrong size for a %s field' % (repr(packed), cls.__name__))
-
- return unpacked
+ def __init__(self, orport, link_protocol):
+ self.link_protocol = link_protocol
+ self._orport = orport
@staticmethod
- def pop(packed):
+ def connect(address, port, link_protocols = DEFAULT_LINK_PROTOCOLS):
"""
- Decodes bytes as this field type, providing it and the remainder.
+ Establishes a connection with the given ORPort.
- :param bytes packed: content to decode
+ :param str address: ip address of the relay
+ :param int port: ORPort of the relay
+ :param tuple link_protocols: acceptable link protocol versions
- :returns: tuple of the form (unpacked, remainder)
-
- :raises: **ValueError** if packed data is malformed
+ :raises:
+ * **ValueError** if address or port are invalid
+ * :class:`stem.SocketError` if we're unable to establish a connection
"""
- raise NotImplementedError('Not yet available')
-
- def __eq__(self, other):
- return hash(self) == hash(other) if isinstance(other, Field) else False
-
- def __ne__(self, other):
- return not self == other
-
-
-class Size(Field):
- """
- Unsigned `struct.pack format
- <https://docs.python.org/2/library/struct.html#format-characters>` for
- network-order fields.
-
- ==================== ===========
- Pack Description
- ==================== ===========
- CHAR Unsigned char (1 byte)
- SHORT Unsigned short (2 bytes)
- LONG Unsigned long (4 bytes)
- LONG_LONG Unsigned long long (8 bytes)
- ==================== ===========
- """
-
- def __init__(self, name, size, pack_format):
- self.name = name
- self.size = size
- self.format = pack_format
-
- @staticmethod
- def pop(packed):
- raise NotImplementedError("Use our constant's unpack() and pop() instead")
-
- def pack(self, content):
- if not isinstance(content, int):
- raise ValueError('Size.pack encodes an integer, but was a %s' % type(content).__name__)
-
- packed = struct.pack(self.format, content)
-
- if self.size != len(packed):
- raise ValueError('%s is the wrong size for a %s field' % (repr(packed), self.name))
-
- return packed
-
- def unpack(self, packed):
- if self.size != len(packed):
- raise ValueError('%s is the wrong size for a %s field' % (repr(packed), self.name))
-
- return struct.unpack(self.format, packed)[0]
-
- def pop(self, packed):
- return self.unpack(packed[:self.size]), packed[self.size:]
-
-
-class Address(Field):
- """
- Relay address.
-
- :var stem.client.AddrType type: address type
- :var int type_int: integer value of the address type
- :var unicode value: address value
- :var bytes value_bin: encoded address value
- """
-
- def __init__(self, value, addr_type = None):
- if addr_type is None:
- if stem.util.connection.is_valid_ipv4_address(value):
- addr_type = AddrType.IPv4
- elif stem.util.connection.is_valid_ipv6_address(value):
- addr_type = AddrType.IPv6
- else:
- raise ValueError('Address type is required unless an IPv4 or IPv6 address')
-
- self.type, self.type_int = AddrType.get(addr_type)
-
- if self.type == AddrType.IPv4:
- if stem.util.connection.is_valid_ipv4_address(value):
- self.value = value
- self.value_bin = ''.join([Size.CHAR.pack(int(v)) for v in value.split('.')])
- else:
- if len(value) != 4:
- raise ValueError('Packed IPv4 addresses should be four bytes, but was: %s' % repr(value))
-
- self.value = '.'.join([str(Size.CHAR.unpack(value[i])) for i in range(4)])
- self.value_bin = value
- elif self.type == AddrType.IPv6:
- if stem.util.connection.is_valid_ipv6_address(value):
- self.value = stem.util.connection.expand_ipv6_address(value).lower()
- self.value_bin = ''.join([Size.SHORT.pack(int(v, 16)) for v in self.value.split(':')])
- else:
- if len(value) != 16:
- raise ValueError('Packed IPv6 addresses should be sixteen bytes, but was: %s' % repr(value))
-
- self.value = ':'.join(['%04x' % Size.SHORT.unpack(value[i * 2:(i + 1) * 2]) for i in range(8)])
- self.value_bin = value
+ if stem.util.connection.is_valid_ipv4_address(address):
+ addr_type = AddrType.IPv4
+ elif stem.util.connection.is_valid_ipv6_address(address):
+ addr_type = AddrType.IPv6
else:
- # The spec doesn't really tell us what form to expect errors to be. For
- # now just leaving the value unset so we can fill it in later when we
- # know what would be most useful.
-
- self.value = None
- self.value_bin = value
-
- def pack(self):
- cell = io.BytesIO()
- cell.write(Size.CHAR.pack(self.type_int))
- cell.write(Size.CHAR.pack(len(self.value_bin)))
- cell.write(self.value_bin)
- return cell.getvalue()
-
- @staticmethod
- def pop(content):
- if not content:
- raise ValueError('Payload empty where an address was expected')
- elif len(content) < 2:
- raise ValueError('Insuffient data for address headers')
-
- addr_type, content = Size.CHAR.pop(content)
- addr_length, content = Size.CHAR.pop(content)
-
- if len(content) < addr_length:
- raise ValueError('Address specified a payload of %i bytes, but only had %i' % (addr_length, len(content)))
-
- addr_value, content = split(content, addr_length)
-
- return Address(addr_value, addr_type), content
-
- def __hash__(self):
- return _hash_attr(self, 'type_int', 'value_bin')
-
-
-class Certificate(Field):
- """
- Relay certificate as defined in tor-spec section 4.2.
-
- :var stem.client.CertType type: certificate type
- :var int type_int: integer value of the certificate type
- :var bytes value: certificate value
- """
-
- def __init__(self, cert_type, value):
- self.type, self.type_int = CertType.get(cert_type)
- self.value = value
+ raise ValueError("'%s' isn't an IPv4 or IPv6 address" % address)
+
+ if not stem.util.connection.is_valid_port(port):
+ raise ValueError("'%s' isn't a valid port" % port)
+ elif not link_protocols:
+ raise ValueError("Connection can't be established without a link protocol.")
+
+ try:
+ conn = stem.socket.RelaySocket(address, port)
+ except stem.SocketError as exc:
+ if 'Connection refused' in str(exc):
+ raise stem.SocketError("Failed to connect to %s:%i. Maybe it isn't an ORPort?" % (address, port))
+ elif 'SSL: UNKNOWN_PROTOCOL' in str(exc):
+ raise stem.SocketError("Failed to SSL authenticate to %s:%i. Maybe it isn't an ORPort?" % (address, port))
+ else:
+ raise
- def pack(self):
- cell = io.BytesIO()
- cell.write(Size.CHAR.pack(self.type_int))
- cell.write(Size.SHORT.pack(len(self.value)))
- cell.write(self.value)
- return cell.getvalue()
+ conn.send(stem.client.cell.VersionsCell(link_protocols).pack())
+ response = conn.recv()
- @staticmethod
- def pop(content):
- cert_type, content = Size.CHAR.pop(content)
- cert_size, content = Size.SHORT.pop(content)
+ # Link negotiation ends right away if we lack a common protocol
+ # version. (#25139)
- if cert_size > len(content):
- raise ValueError('CERTS cell should have a certificate with %i bytes, but only had %i remaining' % (cert_size, len(content)))
+ if not response:
+ conn.close()
+ raise stem.SocketError('Unable to establish a common link protocol with %s:%i' % (address, port))
- cert_bytes, content = split(content, cert_size)
- return Certificate(cert_type, cert_bytes), content
+ versions_reply = stem.client.cell.Cell.pop(response, 2)[0]
+ common_protocols = set(link_protocols).intersection(versions_reply.versions)
- def __hash__(self):
- return _hash_attr(self, 'type_int', 'value')
+ if not common_protocols:
+ conn.close()
+ raise stem.SocketError('Unable to find a common link protocol. We support %s but %s:%i supports %s.' % (', '.join(link_protocols), address, port, ', '.join(versions_reply.versions)))
+ # TODO: we should fill in our address, right?
+ # TODO: what happens if we skip the NETINFO?
-class KDF(collections.namedtuple('KDF', ['key_hash', 'forward_digest', 'backward_digest', 'forward_key', 'backward_key'])):
- """
- Computed KDF-TOR derived values for TAP, CREATE_FAST handshakes, and hidden
- service protocols as defined tor-spec section 5.2.1.
-
- :var bytes key_hash: hash that proves knowledge of our shared key
- :var bytes forward_digest: forward digest hash seed
- :var bytes backward_digest: backward digest hash seed
- :var bytes forward_key: forward encryption key
- :var bytes backward_key: backward encryption key
- """
+ link_protocol = max(common_protocols)
+ conn.send(stem.client.cell.NetinfoCell(Address(address, addr_type), []).pack(link_protocol))
- @staticmethod
- def from_value(key_material):
- # Derived key material, as per...
- #
- # K = H(K0 | [00]) | H(K0 | [01]) | H(K0 | [02]) | ...
+ return Relay(conn, link_protocol)
- derived_key = ''
- counter = 0
+ def is_alive(self):
+ """
+ Checks if our socket is currently connected. This is a pass-through for our
+ socket's :func:`~stem.socket.BaseSocket.is_alive` method.
- while len(derived_key) < KEY_LEN * 2 + HASH_LEN * 3:
- derived_key += hashlib.sha1(key_material + Size.CHAR.pack(counter)).digest()
- counter += 1
+ :returns: **bool** that's **True** if our socket is connected and **False** otherwise
+ """
- key_hash, derived_key = split(derived_key, HASH_LEN)
- forward_digest, derived_key = split(derived_key, HASH_LEN)
- backward_digest, derived_key = split(derived_key, HASH_LEN)
- forward_key, derived_key = split(derived_key, KEY_LEN)
- backward_key, derived_key = split(derived_key, KEY_LEN)
+ return self._orport.is_alive()
- return KDF(key_hash, forward_digest, backward_digest, forward_key, backward_key)
+ def connection_time(self):
+ """
+ Provides the unix timestamp for when our socket was either connected or
+ disconnected. That is to say, the time we connected if we're currently
+ connected and the time we disconnected if we're not connected.
+ :returns: **float** for when we last connected or disconnected, zero if
+ we've never connected
+ """
-class Circuit(collections.namedtuple('Circuit', ['socket', 'id', 'forward_digest', 'backward_digest', 'forward_key', 'backward_key'])):
- """
- Circuit through which requests can be made of a `Tor relay's ORPort
- <https://gitweb.torproject.org/torspec.git/tree/tor-spec.txt>`_.
-
- :var stem.socket.RelaySocket socket: socket through which this circuit has been established
- :var int id: circuit id
- :var hashlib.sha1 forward_digest: digest for forward integrity check
- :var hashlib.sha1 backward_digest: digest for backward integrity check
- :var bytes forward_key: forward encryption key
- :var bytes backward_key: backward encryption key
- """
+ return self._orport.connection_time()
- @staticmethod
- def create(relay_socket, circ_id, link_version):
+ def close(self):
"""
- Constructs a new circuit over the given ORPort.
+ Closes our socket connection. This is a pass-through for our socket's
+ :func:`~stem.socket.BaseSocket.close` method.
"""
- if not stem.prereq.is_crypto_available():
- raise ImportError('Circuit construction requires the cryptography module')
-
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
- from cryptography.hazmat.backends import default_backend
-
- create_fast_cell = stem.client.cell.CreateFastCell(circ_id)
- relay_socket.send(create_fast_cell.pack(link_version))
-
- response = stem.client.cell.Cell.unpack(relay_socket.recv(), link_version)
- created_fast_cells = filter(lambda cell: isinstance(cell, stem.client.cell.CreatedFastCell), response)
-
- if not created_fast_cells:
- raise ValueError('We should get a CREATED_FAST response from a CREATE_FAST request')
-
- created_fast_cell = created_fast_cells[0]
- kdf = KDF.from_value(create_fast_cell.key_material + created_fast_cell.key_material)
- ctr = modes.CTR(ZERO * (algorithms.AES.block_size / 8))
-
- if created_fast_cell.derivative_key != kdf.key_hash:
- raise ValueError('Remote failed to prove that it knows our shared key')
-
- return Circuit(
- relay_socket,
- circ_id,
- hashlib.sha1(kdf.forward_digest),
- hashlib.sha1(kdf.backward_digest),
- Cipher(algorithms.AES(kdf.forward_key), ctr, default_backend()).encryptor(),
- Cipher(algorithms.AES(kdf.backward_key), ctr, default_backend()).decryptor(),
- )
+ return self._orport.close()
+ def __enter__(self):
+ return self
-setattr(Size, 'CHAR', Size('CHAR', 1, '!B'))
-setattr(Size, 'SHORT', Size('SHORT', 2, '!H'))
-setattr(Size, 'LONG', Size('LONG', 4, '!L'))
-setattr(Size, 'LONG_LONG', Size('LONG_LONG', 8, '!Q'))
+ def __exit__(self, exit_type, value, traceback):
+ self.close()
diff --git a/stem/client/cell.py b/stem/client/cell.py
index 2bae4fc5..a018b2a3 100644
--- a/stem/client/cell.py
+++ b/stem/client/cell.py
@@ -44,15 +44,33 @@ import os
import random
import sys
-import stem.client
-
from stem import UNDEFINED
-from stem.client import HASH_LEN, ZERO, Address, Size, split
+from stem.client.datatype import HASH_LEN, ZERO, Address, Certificate, CloseReason, RelayCommand, Size, split
from stem.util import _hash_attr, datetime_to_unix
FIXED_PAYLOAD_LEN = 509
AUTH_CHALLENGE_SIZE = 32
+STREAM_ID_REQUIRED = (
+ RelayCommand.BEGIN,
+ RelayCommand.DATA,
+ RelayCommand.END,
+ RelayCommand.CONNECTED,
+ RelayCommand.RESOLVE,
+ RelayCommand.RESOLVED,
+ RelayCommand.BEGIN_DIR,
+)
+
+STREAM_ID_DISALLOWED = (
+ RelayCommand.EXTEND,
+ RelayCommand.EXTENDED,
+ RelayCommand.TRUNCATE,
+ RelayCommand.TRUNCATED,
+ RelayCommand.DROP,
+ RelayCommand.EXTEND2,
+ RelayCommand.EXTENDED2,
+)
+
class Cell(object):
"""
@@ -297,14 +315,14 @@ class RelayCell(CircuitCell):
def __init__(self, circ_id, command, data, digest = 0, stream_id = 0):
super(RelayCell, self).__init__(circ_id)
- self.command, self.command_int = stem.client.RelayCommand.get(command)
+ self.command, self.command_int = RelayCommand.get(command)
self.data = data
self.digest = digest
self.stream_id = stream_id
- if not stream_id and self.command in stem.client.STREAM_ID_REQUIRED:
+ if not stream_id and self.command in STREAM_ID_REQUIRED:
raise ValueError('%s relay cells require a stream id' % self.command)
- elif stream_id and self.command in stem.client.STREAM_ID_DISALLOWED:
+ elif stream_id and self.command in STREAM_ID_DISALLOWED:
raise ValueError('%s relay cells concern the circuit itself and cannot have a stream id' % self.command)
def pack(self, link_version):
@@ -345,9 +363,9 @@ class DestroyCell(CircuitCell):
VALUE = 4
IS_FIXED_SIZE = True
- def __init__(self, circ_id, reason = stem.client.CloseReason.NONE):
+ def __init__(self, circ_id, reason = CloseReason.NONE):
super(DestroyCell, self).__init__(circ_id)
- self.reason, self.reason_int = stem.client.CloseReason.get(reason)
+ self.reason, self.reason_int = CloseReason.get(reason)
def pack(self, link_version):
return DestroyCell._pack(link_version, Size.CHAR.pack(self.reason_int), self.circ_id)
@@ -610,7 +628,7 @@ class CertsCell(Cell):
if not content:
raise ValueError('CERTS cell indicates it should have %i certificates, but only contained %i' % (cert_count, len(certs)))
- cert, content = stem.client.Certificate.pop(content)
+ cert, content = Certificate.pop(content)
certs.append(cert)
return CertsCell(certs)
diff --git a/stem/client/datatype.py b/stem/client/datatype.py
new file mode 100644
index 00000000..f5805a2a
--- /dev/null
+++ b/stem/client/datatype.py
@@ -0,0 +1,533 @@
+# Copyright 2018, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+"""
+Support for `Tor's ORPort protocol
+<https://gitweb.torproject.org/torspec.git/tree/tor-spec.txt>`_.
+
+**This module only consists of low level components, and is not intended for
+users.** See our :class:`~stem.client.Relay` the API you probably want.
+
+.. versionadded:: 1.7.0
+
+::
+
+ split - splits bytes into substrings
+
+ Field - Packable and unpackable datatype.
+ |- Size - Field of a static size.
+ |- Address - Relay address.
+ |- Certificate - Relay certificate.
+ |
+ |- pack - encodes content
+ |- unpack - decodes content
+ +- pop - decodes content with remainder
+
+ KDF - KDF-TOR derivatived attributes
+ +- from_value - parses key material
+
+.. data:: AddrType (enum)
+
+ Form an address takes.
+
+ ===================== ===========
+ AddressType Description
+ ===================== ===========
+ **HOSTNAME** relay hostname
+ **IPv4** IPv4 address
+ **IPv6** IPv6 address
+ **ERROR_TRANSIENT** temporarily error retrieving address
+ **ERROR_PERMANENT** permanent error retrieving address
+ **UNKNOWN** unrecognized address type
+ ===================== ===========
+
+.. data:: RelayCommand (enum)
+
+ Command concerning streams and circuits we've established with a relay.
+ Commands have two characteristics...
+
+ * **forward/backward**: **forward** commands are issued from the orgin,
+ whereas **backward** come from the relay
+
+ * **stream/circuit**: **steam** commands concern an individual steam, whereas
+ **circuit** concern the entire circuit we've established with a relay
+
+ ===================== ===========
+ RelayCommand Description
+ ===================== ===========
+ **BEGIN** begin a stream (**forward**, **stream**)
+ **DATA** transmit data (**forward/backward**, **stream**)
+ **END** end a stream (**forward/backward**, **stream**)
+ **CONNECTED** BEGIN reply (**backward**, **stream**)
+ **SENDME** ready to accept more cells (**forward/backward**, **stream/circuit**)
+ **EXTEND** extend the circuit through another relay (**forward**, **circuit**)
+ **EXTENDED** EXTEND reply (**backward**, **circuit**)
+ **TRUNCATE** remove last circuit hop (**forward**, **circuit**)
+ **TRUNCATED** TRUNCATE reply (**backward**, **circuit**)
+ **DROP** ignorable no-op (**forward/backward**, **circuit**)
+ **RESOLVE** request DNS resolution (**forward**, **stream**)
+ **RESOLVED** RESOLVE reply (**backward**, **stream**)
+ **BEGIN_DIR** request descriptor (**forward**, **steam**)
+ **EXTEND2** ntor EXTEND request (**forward**, **circuit**)
+ **EXTENDED2** EXTEND2 reply (**backward**, **circuit**)
+ **UNKNOWN** unrecognized command
+ ===================== ===========
+
+.. data:: CertType (enum)
+
+ Relay certificate type.
+
+ ===================== ===========
+ CertType Description
+ ===================== ===========
+ **LINK** link key certificate certified by RSA1024 identity
+ **IDENTITY** RSA1024 Identity certificate
+ **AUTHENTICATE** RSA1024 AUTHENTICATE cell link certificate
+ **UNKNOWN** unrecognized certificate type
+ ===================== ===========
+
+.. data:: CloseReason (enum)
+
+ Reason a relay is closed.
+
+ ===================== ===========
+ CloseReason Description
+ ===================== ===========
+ **NONE** no reason given
+ **PROTOCOL** tor protocol violation
+ **INTERNAL** internal error
+ **REQUESTED** client sent a TRUNCATE command
+ **HIBERNATING** relay suspended, trying to save bandwidth
+ **RESOURCELIMIT** out of memory, sockets, or circuit IDs
+ **CONNECTFAILED** unable to reach relay
+ **OR_IDENTITY** connected, but its OR identity was not as expected
+ **OR_CONN_CLOSED** connection that was carrying this circuit died
+ **FINISHED** circuit has expired for being dirty or old
+ **TIMEOUT** circuit construction took too long
+ **DESTROYED** circuit was destroyed without a client TRUNCATE
+ **NOSUCHSERVICE** request was for an unknown hidden service
+ **UNKNOWN** unrecognized reason
+ ===================== ===========
+"""
+
+import collections
+import hashlib
+import io
+import struct
+
+import stem.prereq
+import stem.util.connection
+import stem.util.enum
+
+from stem.util import _hash_attr
+
+ZERO = '\x00'
+HASH_LEN = 20
+KEY_LEN = 16
+
+
+class _IntegerEnum(stem.util.enum.Enum):
+ """
+ Integer backed enumeration. Enumerations of this type always have an implicit
+ **UNKNOWN** value for integer values that lack a mapping.
+ """
+
+ def __init__(self, *args):
+ self._enum_to_int = {}
+ self._int_to_enum = {}
+ parent_args = []
+
+ for entry in args:
+ if len(entry) == 2:
+ enum, int_val = entry
+ str_val = enum
+ elif len(entry) == 3:
+ enum, str_val, int_val = entry
+ else:
+ raise ValueError('IntegerEnums can only be constructed with two or three value tuples: %s' % repr(entry))
+
+ self._enum_to_int[str_val] = int_val
+ self._int_to_enum[int_val] = str_val
+ parent_args.append((enum, str_val))
+
+ parent_args.append(('UNKNOWN', 'UNKNOWN'))
+ super(_IntegerEnum, self).__init__(*parent_args)
+
+ def get(self, val):
+ """
+ Privides the (enum, int_value) tuple for a given value.
+ """
+
+ if isinstance(val, int):
+ return self._int_to_enum.get(val, self.UNKNOWN), val
+ elif val in self:
+ return val, self._enum_to_int.get(val, val)
+ else:
+ raise ValueError("Invalid enumeration '%s', options are %s" % (val, ', '.join(self)))
+
+
+AddrType = _IntegerEnum(
+ ('HOSTNAME', 0),
+ ('IPv4', 4),
+ ('IPv6', 6),
+ ('ERROR_TRANSIENT', 16),
+ ('ERROR_PERMANENT', 17),
+)
+
+RelayCommand = _IntegerEnum(
+ ('BEGIN', 'RELAY_BEGIN', 1),
+ ('DATA', 'RELAY_DATA', 2),
+ ('END', 'RELAY_END', 3),
+ ('CONNECTED', 'RELAY_CONNECTED', 4),
+ ('SENDME', 'RELAY_SENDME', 5),
+ ('EXTEND', 'RELAY_EXTEND', 6),
+ ('EXTENDED', 'RELAY_EXTENDED', 7),
+ ('TRUNCATE', 'RELAY_TRUNCATE', 8),
+ ('TRUNCATED', 'RELAY_TRUNCATED', 9),
+ ('DROP', 'RELAY_DROP', 10),
+ ('RESOLVE', 'RELAY_RESOLVE', 11),
+ ('RESOLVED', 'RELAY_RESOLVED', 12),
+ ('BEGIN_DIR', 'RELAY_BEGIN_DIR', 13),
+ ('EXTEND2', 'RELAY_EXTEND2', 14),
+ ('EXTENDED2', 'RELAY_EXTENDED2', 15),
+)
+
+CertType = _IntegerEnum(
+ ('LINK', 1),
+ ('IDENTITY', 2),
+ ('AUTHENTICATE', 3),
+)
+
+CloseReason = _IntegerEnum(
+ ('NONE', 0),
+ ('PROTOCOL', 1),
+ ('INTERNAL', 2),
+ ('REQUESTED', 3),
+ ('HIBERNATING', 4),
+ ('RESOURCELIMIT', 5),
+ ('CONNECTFAILED', 6),
+ ('OR_IDENTITY', 7),
+ ('OR_CONN_CLOSED', 8),
+ ('FINISHED', 9),
+ ('TIMEOUT', 10),
+ ('DESTROYED', 11),
+ ('NOSUCHSERVICE', 12),
+)
+
+
+def split(content, size):
+ """
+ Simple split of bytes into two substrings.
+
+ :param bytes content: string to split
+ :param int size: index to split the string on
+
+ :returns: two value tuple with the split bytes
+ """
+
+ return content[:size], content[size:]
+
+
+class Field(object):
+ """
+ Packable and unpackable datatype.
+ """
+
+ def pack(self):
+ """
+ Encodes field into bytes.
+
+ :returns: **bytes** that can be communicated over Tor's ORPort
+
+ :raises: **ValueError** if incorrect type or size
+ """
+
+ raise NotImplementedError('Not yet available')
+
+ @classmethod
+ def unpack(cls, packed):
+ """
+ Decodes bytes into a field of this type.
+
+ :param bytes packed: content to decode
+
+ :returns: instance of this class
+
+ :raises: **ValueError** if packed data is malformed
+ """
+
+ unpacked, remainder = cls.pop(packed)
+
+ if remainder:
+ raise ValueError('%s is the wrong size for a %s field' % (repr(packed), cls.__name__))
+
+ return unpacked
+
+ @staticmethod
+ def pop(packed):
+ """
+ Decodes bytes as this field type, providing it and the remainder.
+
+ :param bytes packed: content to decode
+
+ :returns: tuple of the form (unpacked, remainder)
+
+ :raises: **ValueError** if packed data is malformed
+ """
+
+ raise NotImplementedError('Not yet available')
+
+ def __eq__(self, other):
+ return hash(self) == hash(other) if isinstance(other, Field) else False
+
+ def __ne__(self, other):
+ return not self == other
+
+
+class Size(Field):
+ """
+ Unsigned `struct.pack format
+ <https://docs.python.org/2/library/struct.html#format-characters>` for
+ network-order fields.
+
+ ==================== ===========
+ Pack Description
+ ==================== ===========
+ CHAR Unsigned char (1 byte)
+ SHORT Unsigned short (2 bytes)
+ LONG Unsigned long (4 bytes)
+ LONG_LONG Unsigned long long (8 bytes)
+ ==================== ===========
+ """
+
+ def __init__(self, name, size, pack_format):
+ self.name = name
+ self.size = size
+ self.format = pack_format
+
+ @staticmethod
+ def pop(packed):
+ raise NotImplementedError("Use our constant's unpack() and pop() instead")
+
+ def pack(self, content):
+ if not isinstance(content, int):
+ raise ValueError('Size.pack encodes an integer, but was a %s' % type(content).__name__)
+
+ packed = struct.pack(self.format, content)
+
+ if self.size != len(packed):
+ raise ValueError('%s is the wrong size for a %s field' % (repr(packed), self.name))
+
+ return packed
+
+ def unpack(self, packed):
+ if self.size != len(packed):
+ raise ValueError('%s is the wrong size for a %s field' % (repr(packed), self.name))
+
+ return struct.unpack(self.format, packed)[0]
+
+ def pop(self, packed):
+ return self.unpack(packed[:self.size]), packed[self.size:]
+
+
+class Address(Field):
+ """
+ Relay address.
+
+ :var stem.client.AddrType type: address type
+ :var int type_int: integer value of the address type
+ :var unicode value: address value
+ :var bytes value_bin: encoded address value
+ """
+
+ def __init__(self, value, addr_type = None):
+ if addr_type is None:
+ if stem.util.connection.is_valid_ipv4_address(value):
+ addr_type = AddrType.IPv4
+ elif stem.util.connection.is_valid_ipv6_address(value):
+ addr_type = AddrType.IPv6
+ else:
+ raise ValueError('Address type is required unless an IPv4 or IPv6 address')
+
+ self.type, self.type_int = AddrType.get(addr_type)
+
+ if self.type == AddrType.IPv4:
+ if stem.util.connection.is_valid_ipv4_address(value):
+ self.value = value
+ self.value_bin = ''.join([Size.CHAR.pack(int(v)) for v in value.split('.')])
+ else:
+ if len(value) != 4:
+ raise ValueError('Packed IPv4 addresses should be four bytes, but was: %s' % repr(value))
+
+ self.value = '.'.join([str(Size.CHAR.unpack(value[i])) for i in range(4)])
+ self.value_bin = value
+ elif self.type == AddrType.IPv6:
+ if stem.util.connection.is_valid_ipv6_address(value):
+ self.value = stem.util.connection.expand_ipv6_address(value).lower()
+ self.value_bin = ''.join([Size.SHORT.pack(int(v, 16)) for v in self.value.split(':')])
+ else:
+ if len(value) != 16:
+ raise ValueError('Packed IPv6 addresses should be sixteen bytes, but was: %s' % repr(value))
+
+ self.value = ':'.join(['%04x' % Size.SHORT.unpack(value[i * 2:(i + 1) * 2]) for i in range(8)])
+ self.value_bin = value
+ else:
+ # The spec doesn't really tell us what form to expect errors to be. For
+ # now just leaving the value unset so we can fill it in later when we
+ # know what would be most useful.
+
+ self.value = None
+ self.value_bin = value
+
+ def pack(self):
+ cell = io.BytesIO()
+ cell.write(Size.CHAR.pack(self.type_int))
+ cell.write(Size.CHAR.pack(len(self.value_bin)))
+ cell.write(self.value_bin)
+ return cell.getvalue()
+
+ @staticmethod
+ def pop(content):
+ if not content:
+ raise ValueError('Payload empty where an address was expected')
+ elif len(content) < 2:
+ raise ValueError('Insuffient data for address headers')
+
+ addr_type, content = Size.CHAR.pop(content)
+ addr_length, content = Size.CHAR.pop(content)
+
+ if len(content) < addr_length:
+ raise ValueError('Address specified a payload of %i bytes, but only had %i' % (addr_length, len(content)))
+
+ addr_value, content = split(content, addr_length)
+
+ return Address(addr_value, addr_type), content
+
+ def __hash__(self):
+ return _hash_attr(self, 'type_int', 'value_bin')
+
+
+class Certificate(Field):
+ """
+ Relay certificate as defined in tor-spec section 4.2.
+
+ :var stem.client.CertType type: certificate type
+ :var int type_int: integer value of the certificate type
+ :var bytes value: certificate value
+ """
+
+ def __init__(self, cert_type, value):
+ self.type, self.type_int = CertType.get(cert_type)
+ self.value = value
+
+ def pack(self):
+ cell = io.BytesIO()
+ cell.write(Size.CHAR.pack(self.type_int))
+ cell.write(Size.SHORT.pack(len(self.value)))
+ cell.write(self.value)
+ return cell.getvalue()
+
+ @staticmethod
+ def pop(content):
+ cert_type, content = Size.CHAR.pop(content)
+ cert_size, content = Size.SHORT.pop(content)
+
+ if cert_size > len(content):
+ raise ValueError('CERTS cell should have a certificate with %i bytes, but only had %i remaining' % (cert_size, len(content)))
+
+ cert_bytes, content = split(content, cert_size)
+ return Certificate(cert_type, cert_bytes), content
+
+ def __hash__(self):
+ return _hash_attr(self, 'type_int', 'value')
+
+
+class KDF(collections.namedtuple('KDF', ['key_hash', 'forward_digest', 'backward_digest', 'forward_key', 'backward_key'])):
+ """
+ Computed KDF-TOR derived values for TAP, CREATE_FAST handshakes, and hidden
+ service protocols as defined tor-spec section 5.2.1.
+
+ :var bytes key_hash: hash that proves knowledge of our shared key
+ :var bytes forward_digest: forward digest hash seed
+ :var bytes backward_digest: backward digest hash seed
+ :var bytes forward_key: forward encryption key
+ :var bytes backward_key: backward encryption key
+ """
+
+ @staticmethod
+ def from_value(key_material):
+ # Derived key material, as per...
+ #
+ # K = H(K0 | [00]) | H(K0 | [01]) | H(K0 | [02]) | ...
+
+ derived_key = ''
+ counter = 0
+
+ while len(derived_key) < KEY_LEN * 2 + HASH_LEN * 3:
+ derived_key += hashlib.sha1(key_material + Size.CHAR.pack(counter)).digest()
+ counter += 1
+
+ key_hash, derived_key = split(derived_key, HASH_LEN)
+ forward_digest, derived_key = split(derived_key, HASH_LEN)
+ backward_digest, derived_key = split(derived_key, HASH_LEN)
+ forward_key, derived_key = split(derived_key, KEY_LEN)
+ backward_key, derived_key = split(derived_key, KEY_LEN)
+
+ return KDF(key_hash, forward_digest, backward_digest, forward_key, backward_key)
+
+
+class Circuit(collections.namedtuple('Circuit', ['socket', 'id', 'forward_digest', 'backward_digest', 'forward_key', 'backward_key'])):
+ """
+ Circuit through which requests can be made of a `Tor relay's ORPort
+ <https://gitweb.torproject.org/torspec.git/tree/tor-spec.txt>`_.
+
+ :var stem.socket.RelaySocket socket: socket through which this circuit has been established
+ :var int id: circuit id
+ :var hashlib.sha1 forward_digest: digest for forward integrity check
+ :var hashlib.sha1 backward_digest: digest for backward integrity check
+ :var bytes forward_key: forward encryption key
+ :var bytes backward_key: backward encryption key
+ """
+
+ @staticmethod
+ def create(relay_socket, circ_id, link_version):
+ """
+ Constructs a new circuit over the given ORPort.
+ """
+
+ if not stem.prereq.is_crypto_available():
+ raise ImportError('Circuit construction requires the cryptography module')
+
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+ from cryptography.hazmat.backends import default_backend
+
+ create_fast_cell = stem.client.cell.CreateFastCell(circ_id)
+ relay_socket.send(create_fast_cell.pack(link_version))
+
+ response = stem.client.cell.Cell.unpack(relay_socket.recv(), link_version)
+ created_fast_cells = filter(lambda cell: isinstance(cell, stem.client.cell.CreatedFastCell), response)
+
+ if not created_fast_cells:
+ raise ValueError('We should get a CREATED_FAST response from a CREATE_FAST request')
+
+ created_fast_cell = created_fast_cells[0]
+ kdf = KDF.from_value(create_fast_cell.key_material + created_fast_cell.key_material)
+ ctr = modes.CTR(ZERO * (algorithms.AES.block_size / 8))
+
+ if created_fast_cell.derivative_key != kdf.key_hash:
+ raise ValueError('Remote failed to prove that it knows our shared key')
+
+ return Circuit(
+ relay_socket,
+ circ_id,
+ hashlib.sha1(kdf.forward_digest),
+ hashlib.sha1(kdf.backward_digest),
+ Cipher(algorithms.AES(kdf.forward_key), ctr, default_backend()).encryptor(),
+ Cipher(algorithms.AES(kdf.backward_key), ctr, default_backend()).decryptor(),
+ )
+
+
+setattr(Size, 'CHAR', Size('CHAR', 1, '!B'))
+setattr(Size, 'SHORT', Size('SHORT', 2, '!H'))
+setattr(Size, 'LONG', Size('LONG', 4, '!L'))
+setattr(Size, 'LONG_LONG', Size('LONG_LONG', 8, '!Q'))
diff --git a/stem/relay.py b/stem/relay.py
deleted file mode 100644
index 14b7833f..00000000
--- a/stem/relay.py
+++ /dev/null
@@ -1,137 +0,0 @@
-# Copyright 2018, Damian Johnson and The Tor Project
-# See LICENSE for licensing information
-
-"""
-Interaction with a Tor relay's ORPort. :class:`~stem.relay.Relay` is
-a wrapper for :class:`~stem.socket.RelaySocket`, much the same way as
-:class:`~stem.control.Controller` provides higher level functions for
-:class:`~stem.socket.ControlSocket`.
-
-.. versionadded:: 1.7.0
-
-::
-
- Relay - Connection with a tor relay's ORPort.
- | +- connect - Establishes a connection with a relay.
- |
- |- is_alive - reports if our connection is open or closed
- |- connection_time - time when we last connected or disconnected
- +- close - shuts down our connection
-"""
-
-import stem
-import stem.client
-import stem.client.cell
-import stem.socket
-import stem.util.connection
-
-DEFAULT_LINK_PROTOCOLS = (3, 4, 5)
-
-
-class Relay(object):
- """
- Connection with a Tor relay's ORPort.
-
- :var int link_protocol: link protocol version we established
- """
-
- def __init__(self, orport, link_protocol):
- self.link_protocol = link_protocol
- self._orport = orport
-
- @staticmethod
- def connect(address, port, link_protocols = DEFAULT_LINK_PROTOCOLS):
- """
- Establishes a connection with the given ORPort.
-
- :param str address: ip address of the relay
- :param int port: ORPort of the relay
- :param tuple link_protocols: acceptable link protocol versions
-
- :raises:
- * **ValueError** if address or port are invalid
- * :class:`stem.SocketError` if we're unable to establish a connection
- """
-
- if stem.util.connection.is_valid_ipv4_address(address):
- addr_type = stem.client.AddrType.IPv4
- elif stem.util.connection.is_valid_ipv6_address(address):
- addr_type = stem.client.AddrType.IPv6
- else:
- raise ValueError("'%s' isn't an IPv4 or IPv6 address" % address)
-
- if not stem.util.connection.is_valid_port(port):
- raise ValueError("'%s' isn't a valid port" % port)
- elif not link_protocols:
- raise ValueError("Connection can't be established without a link protocol.")
-
- try:
- conn = stem.socket.RelaySocket(address, port)
- except stem.SocketError as exc:
- if 'Connection refused' in str(exc):
- raise stem.SocketError("Failed to connect to %s:%i. Maybe it isn't an ORPort?" % (address, port))
- elif 'SSL: UNKNOWN_PROTOCOL' in str(exc):
- raise stem.SocketError("Failed to SSL authenticate to %s:%i. Maybe it isn't an ORPort?" % (address, port))
- else:
- raise
-
- conn.send(stem.client.cell.VersionsCell(link_protocols).pack())
- response = conn.recv()
-
- # Link negotiation ends right away if we lack a common protocol
- # version. (#25139)
-
- if not response:
- conn.close()
- raise stem.SocketError('Unable to establish a common link protocol with %s:%i' % (address, port))
-
- versions_reply = stem.client.cell.Cell.pop(response, 2)[0]
- common_protocols = set(link_protocols).intersection(versions_reply.versions)
-
- if not common_protocols:
- conn.close()
- raise stem.SocketError('Unable to find a common link protocol. We support %s but %s:%i supports %s.' % (', '.join(link_protocols), address, port, ', '.join(versions_reply.versions)))
-
- # TODO: we should fill in our address, right?
- # TODO: what happens if we skip the NETINFO?
-
- link_protocol = max(common_protocols)
- conn.send(stem.client.cell.NetinfoCell(stem.client.Address(address, addr_type), []).pack(link_protocol))
-
- return Relay(conn, link_protocol)
-
- def is_alive(self):
- """
- Checks if our socket is currently connected. This is a pass-through for our
- socket's :func:`~stem.socket.BaseSocket.is_alive` method.
-
- :returns: **bool** that's **True** if our socket is connected and **False** otherwise
- """
-
- return self._orport.is_alive()
-
- def connection_time(self):
- """
- Provides the unix timestamp for when our socket was either connected or
- disconnected. That is to say, the time we connected if we're currently
- connected and the time we disconnected if we're not connected.
-
- :returns: **float** for when we last connected or disconnected, zero if
- we've never connected
- """
-
- return self._orport.connection_time()
-
- def close(self):
- """
- Closes our socket connection. This is a pass-through for our socket's
- :func:`~stem.socket.BaseSocket.close` method.
- """
-
- return self._orport.close()
-
- def __enter__(self):
- return self
-
- def __exit__(self, exit_type, value, traceback):
- self.close()
diff --git a/test/integ/client/__init__.py b/test/integ/client/__init__.py
index 8d77a653..345e69fc 100644
--- a/test/integ/client/__init__.py
+++ b/test/integ/client/__init__.py
@@ -1,5 +1,5 @@
"""
-Integration tests for tor's ORPort (stem.relay and stem.client).
+Integration tests for stem.client.
"""
__all__ = [
diff --git a/test/integ/client/connection.py b/test/integ/client/connection.py
index f5399a71..86a9cbcc 100644
--- a/test/integ/client/connection.py
+++ b/test/integ/client/connection.py
@@ -8,7 +8,7 @@ import unittest
import stem
import test.runner
-from stem.relay import Relay
+from stem.client import Relay
class TestConnection(unittest.TestCase):
diff --git a/test/settings.cfg b/test/settings.cfg
index 6d543bac..aaab0790 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -146,7 +146,7 @@ pycodestyle.ignore test/unit/util/connection.py => W291: _tor tor 158
# issue.
pyflakes.ignore run_tests.py => 'unittest' imported but unused
-pyflakes.ignore stem/client/__init__.py => redefinition of unused 'pop' from *
+pyflakes.ignore stem/client/datatype.py => redefinition of unused 'pop' from *
pyflakes.ignore stem/util/__init__.py => undefined name 'long'
pyflakes.ignore stem/util/__init__.py => undefined name 'unicode'
pyflakes.ignore stem/control.py => undefined name 'controller'
diff --git a/test/unit/client/address.py b/test/unit/client/address.py
index c92f7722..f3da4971 100644
--- a/test/unit/client/address.py
+++ b/test/unit/client/address.py
@@ -6,7 +6,7 @@ import collections
import re
import unittest
-from stem.client import AddrType, Address
+from stem.client.datatype import AddrType, Address
ExpectedAddress = collections.namedtuple('ExpectedAddress', ['type', 'type_int', 'value', 'value_bin'])
diff --git a/test/unit/client/cell.py b/test/unit/client/cell.py
index 1fde76cf..f53355dd 100644
--- a/test/unit/client/cell.py
+++ b/test/unit/client/cell.py
@@ -6,7 +6,7 @@ import datetime
import os
import unittest
-from stem.client import ZERO, CertType, CloseReason, Address, Certificate
+from stem.client.datatype import ZERO, CertType, CloseReason, Address, Certificate
from test.unit.client import test_data
from stem.client.cell import (
diff --git a/test/unit/client/certificate.py b/test/unit/client/certificate.py
index 873de51d..b6782acc 100644
--- a/test/unit/client/certificate.py
+++ b/test/unit/client/certificate.py
@@ -4,7 +4,7 @@ Unit tests for stem.client.Certificate.
import unittest
-from stem.client import CertType, Certificate
+from stem.client.datatype import CertType, Certificate
class TestCertificate(unittest.TestCase):
diff --git a/test/unit/client/kdf.py b/test/unit/client/kdf.py
index 56665d31..f3922538 100644
--- a/test/unit/client/kdf.py
+++ b/test/unit/client/kdf.py
@@ -4,7 +4,7 @@ Unit tests for stem.client.KDF.
import unittest
-import stem.client
+from stem.client.datatype import KDF
KEY_1 = '\xec\xec.\xeb7R\xf2\n\xcb\xce\x97\xf4\x86\x82\x19#\x10\x0f\x08\xf0\xa2Z\xdeJ\x8f2\x8cc\xf6\xfa\x0e\t\x83f\xc5\xe2\xb3\x94\xa8\x13'
KEY_2 = '\xe0v\xe4\xfaTB\x91\x1c\x81Gz\xa0\tI\xcb{\xc56\xcfV\xc2\xa0\x19\x9c\x98\x9a\x06\x0e\xc5\xfa\xb0z\x83\xa6\x10\xf6r"<b'
@@ -12,14 +12,14 @@ KEY_2 = '\xe0v\xe4\xfaTB\x91\x1c\x81Gz\xa0\tI\xcb{\xc56\xcfV\xc2\xa0\x19\x9c\x98
class TestKDF(unittest.TestCase):
def test_parsing(self):
- k1 = stem.client.KDF.from_value(KEY_1)
+ k1 = KDF.from_value(KEY_1)
self.assertEqual('\xca+\x81\x05\x14\x9d)o\xa6\x82\xe9B\xa8?\xf2\xaf\x85\x1b]6', k1.key_hash)
self.assertEqual('\xac\xcc\xbc\x91\xb1\xaf\xd7\xe0\xe9\x9dF#\xd8\xdbz\xe8\xe6\xca\x83,', k1.forward_digest)
self.assertEqual('*\xe5scX\xbb+\xca \xcb\xa4\xbc\xad\x0f\x95\x0cO\xcc\xac\xf1', k1.backward_digest)
self.assertEqual('\xc3\xbe\xc9\xe1\xf4\x90f\xdai\xf3\xf3\xf5\x14\xb5\xb9\x03', k1.forward_key)
self.assertEqual('U\xaf\x1e\x1b\xb1q||\x86A<_\xf7\xa0%\x86', k1.backward_key)
- k2 = stem.client.KDF.from_value(KEY_1)
+ k2 = KDF.from_value(KEY_1)
self.assertEqual('\xca+\x81\x05\x14\x9d)o\xa6\x82\xe9B\xa8?\xf2\xaf\x85\x1b]6', k2.key_hash)
self.assertEqual('\xac\xcc\xbc\x91\xb1\xaf\xd7\xe0\xe9\x9dF#\xd8\xdbz\xe8\xe6\xca\x83,', k2.forward_digest)
self.assertEqual('*\xe5scX\xbb+\xca \xcb\xa4\xbc\xad\x0f\x95\x0cO\xcc\xac\xf1', k2.backward_digest)
diff --git a/test/unit/client/size.py b/test/unit/client/size.py
index 6078139c..59416662 100644
--- a/test/unit/client/size.py
+++ b/test/unit/client/size.py
@@ -5,7 +5,7 @@ Unit tests for stem.client.Size.
import re
import unittest
-from stem.client import Size
+from stem.client.datatype import Size
class TestSize(unittest.TestCase):
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits