[tor-commits] [stem/master] Support DESTROY cells

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


commit aeb17f9d150a7e3f404a37f57c29759325ab3221
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Jan 22 12:10:21 2018 -0800

    Support DESTROY cells
---
 stem/client/__init__.py  | 40 +++++++++++++++++++++++++
 stem/client/cell.py      | 76 ++++++++++++++++++++++++++++++++++++++++++++++--
 test/unit/client/cell.py | 20 ++++++++++++-
 3 files changed, 132 insertions(+), 4 deletions(-)

diff --git a/stem/client/__init__.py b/stem/client/__init__.py
index 1fd05f9d..01c6b749 100644
--- a/stem/client/__init__.py
+++ b/stem/client/__init__.py
@@ -49,6 +49,29 @@ a wrapper for :class:`~stem.socket.RelaySocket`, much the same way as
   **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 io
@@ -81,6 +104,23 @@ CertType = stem.util.enum.UppercaseEnum(
   'UNKNOWN',
 )
 
+CloseReason = stem.util.enum.UppercaseEnum(
+  'NONE',
+  'PROTOCOL',
+  'INTERNAL',
+  'REQUESTED',
+  'HIBERNATING',
+  'RESOURCELIMIT',
+  'CONNECTFAILED',
+  'OR_IDENTITY',
+  'OR_CONN_CLOSED',
+  'FINISHED',
+  'TIMEOUT',
+  'DESTROYED',
+  'NOSUCHSERVICE',
+  'UNKNOWN',
+)
+
 
 def split(content, size):
   """
diff --git a/stem/client/cell.py b/stem/client/cell.py
index 6f107b79..76a75108 100644
--- a/stem/client/cell.py
+++ b/stem/client/cell.py
@@ -44,7 +44,7 @@ import random
 import sys
 
 from stem import UNDEFINED
-from stem.client import ZERO, Address, Certificate, Size, split
+from stem.client import ZERO, Address, Certificate, CloseReason, Size, split
 from stem.util import _hash_attr, datetime_to_unix
 
 FIXED_PAYLOAD_LEN = 509
@@ -261,10 +261,80 @@ class RelayCell(CircuitCell):
 
 
 class DestroyCell(CircuitCell):
+  """
+  Closes the given circuit.
+
+  :var stem.client.CloseReason reason: reason the circuit is being closed
+  :var int reason_int: integer value of our closure reason
+  """
+
   NAME = 'DESTROY'
   VALUE = 4
   IS_FIXED_SIZE = True
 
+  REASON_FOR_INT = {
+    0: CloseReason.NONE,
+    1: CloseReason.PROTOCOL,
+    2: CloseReason.INTERNAL,
+    3: CloseReason.REQUESTED,
+    4: CloseReason.HIBERNATING,
+    5: CloseReason.RESOURCELIMIT,
+    6: CloseReason.CONNECTFAILED,
+    7: CloseReason.OR_IDENTITY,
+    8: CloseReason.OR_CONN_CLOSED,
+    9: CloseReason.FINISHED,
+    10: CloseReason.TIMEOUT,
+    11: CloseReason.DESTROYED,
+    12: CloseReason.NOSUCHSERVICE,
+  }
+
+  INT_FOR_REASON = dict((v, k) for k, v in REASON_FOR_INT.items())
+
+  def __init__(self, circ_id, reason):
+    super(DestroyCell, self).__init__(circ_id)
+
+    if isinstance(reason, int):
+      self.reason = DestroyCell.REASON_FOR_INT.get(reason, CloseReason.UNKNOWN)
+      self.reason_int = reason
+    elif reason in CloseReason:
+      self.reason = reason
+      self.reason_int = DestroyCell.INT_FOR_REASON.get(reason, -1)
+    else:
+      raise ValueError('Invalid closure reason: %s' % reason)
+
+  @classmethod
+  def pack(cls, link_version, circ_id, reason = CloseReason.NONE):
+    """
+    Provides payload to close the given circuit.
+
+    :param int link_version: link protocol version
+    :param int circ_id: circuit id
+    :param stem.client.CloseReason reason: reason to close the circuit
+
+    :returns: **bytes** to close the circuit
+    """
+
+    reason = DestroyCell.INT_FOR_REASON.get(reason, reason)
+
+    if not isinstance(reason, int):
+      raise ValueError('Invalid closure reason: %s' % reason)
+
+    return cls._pack(link_version, Size.CHAR.pack(reason), circ_id)
+
+  @classmethod
+  def _unpack(cls, content, circ_id, link_version):
+    content = content.rstrip(ZERO)
+
+    if not content:
+      content = ZERO
+    elif len(content) > 1:
+      raise ValueError('Circuit closure reason should be a single byte, but was %i' % len(content))
+
+    return DestroyCell(circ_id, Size.CHAR.unpack(content))
+
+  def __hash__(self):
+    return _hash_attr(self, 'circ_id', 'reason_int')
+
 
 class CreateFastCell(CircuitCell):
   """
@@ -309,7 +379,7 @@ class CreateFastCell(CircuitCell):
     return CreateFastCell(circ_id, content)
 
   def __hash__(self):
-    return _hash_attr(self, 'key_material')
+    return _hash_attr(self, 'circ_id', 'key_material')
 
 
 class CreatedFastCell(CircuitCell):
@@ -362,7 +432,7 @@ class CreatedFastCell(CircuitCell):
     return CreatedFastCell(circ_id, content[:HASH_LEN], content[HASH_LEN:])
 
   def __hash__(self):
-    return _hash_attr(self, 'derivative_key', 'key_material')
+    return _hash_attr(self, 'circ_id', 'derivative_key', 'key_material')
 
 
 class VersionsCell(Cell):
diff --git a/test/unit/client/cell.py b/test/unit/client/cell.py
index 577114c3..5b8db2bb 100644
--- a/test/unit/client/cell.py
+++ b/test/unit/client/cell.py
@@ -6,13 +6,14 @@ import datetime
 import os
 import unittest
 
-from stem.client import ZERO, AddrType, CertType, Address, Certificate
+from stem.client import ZERO, AddrType, CertType, CloseReason, Address, Certificate
 from test.unit.client import test_data
 
 from stem.client.cell import (
   FIXED_PAYLOAD_LEN,
   Cell,
   PaddingCell,
+  DestroyCell,
   CreateFastCell,
   CreatedFastCell,
   VersionsCell,
@@ -29,6 +30,11 @@ PADDING_CELLS = {
   '\x00\x00\x00' + RANDOM_PAYLOAD: RANDOM_PAYLOAD,
 }
 
+DESTROY_CELLS = {
+  '\x80\x00\x00\x00\x04\x00' + ZERO * 508: (2147483648, CloseReason.NONE, 0),
+  '\x80\x00\x00\x00\x04\x03' + ZERO * 508: (2147483648, CloseReason.REQUESTED, 3),
+}
+
 CREATE_FAST_CELLS = {
   ('\x80\x00\x00\x00\x05\x92O\x0c\xcb\xa8\xac\xfb\xc9\x7f\xd0\rz\x1a\x03u\x91\xceas\xce' + ZERO * 489): (2147483648, '\x92O\x0c\xcb\xa8\xac\xfb\xc9\x7f\xd0\rz\x1a\x03u\x91\xceas\xce'),
 }
@@ -128,6 +134,18 @@ class TestCell(unittest.TestCase):
       self.assertEqual(cell_bytes, PaddingCell.pack(2, payload))
       self.assertEqual(payload, Cell.unpack(cell_bytes, 2)[0].payload)
 
+  def test_destroy_packing(self):
+    for cell_bytes, (circ_id, reason, reason_int) in DESTROY_CELLS.items():
+      self.assertEqual(cell_bytes, DestroyCell.pack(5, circ_id, reason))
+      self.assertEqual(cell_bytes, DestroyCell.pack(5, circ_id, reason_int))
+
+      cell = Cell.unpack(cell_bytes, 5)[0]
+      self.assertEqual(circ_id, cell.circ_id)
+      self.assertEqual(reason, cell.reason)
+      self.assertEqual(reason_int, cell.reason_int)
+
+    self.assertRaisesRegexp(ValueError, 'Circuit closure reason should be a single byte, but was 2', Cell.unpack, '\x80\x00\x00\x00\x04\x01\x01' + ZERO * 507, 5)
+
   def test_create_fast(self):
     for cell_bytes, (circ_id, key_material) in CREATE_FAST_CELLS.items():
       self.assertEqual(cell_bytes, CreateFastCell.pack(5, circ_id, key_material))





More information about the tor-commits mailing list