[tor-commits] [stem/master] Move Relay class into stem.client

atagar at torproject.org atagar at torproject.org
Wed Feb 7 19:44:51 UTC 2018


commit 9b3a868a80e91527d9cdf276b3e27eef1bf34b02
Author: Damian Johnson <atagar at torproject.org>
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):





More information about the tor-commits mailing list