update path, add python unrar
This commit is contained in:
parent
642ba49f68
commit
00165d302e
862 changed files with 804 additions and 6 deletions
287
Shared/lib/python3.7/site-packages/stem/client/__init__.py
Normal file
287
Shared/lib/python3.7/site-packages/stem/client/__init__.py
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
# Copyright 2018, Damian Johnson and The Tor Project
|
||||
# See LICENSE for licensing information
|
||||
|
||||
"""
|
||||
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
|
||||
|
||||
::
|
||||
|
||||
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
|
||||
|
|
||||
+- create_circuit - establishes a new circuit
|
||||
|
||||
Circuit - Circuit we've established through a relay.
|
||||
|- send - sends a message through this circuit
|
||||
+- close - closes this circuit
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import threading
|
||||
|
||||
import stem
|
||||
import stem.client.cell
|
||||
import stem.socket
|
||||
import stem.util.connection
|
||||
|
||||
from stem.client.datatype import ZERO, LinkProtocol, Address, KDF, split
|
||||
|
||||
__all__ = [
|
||||
'cell',
|
||||
'datatype',
|
||||
]
|
||||
|
||||
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 = LinkProtocol(link_protocol)
|
||||
self._orport = orport
|
||||
self._orport_lock = threading.RLock()
|
||||
self._circuits = {}
|
||||
|
||||
@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
|
||||
"""
|
||||
|
||||
relay_addr = 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))
|
||||
|
||||
# If not an ORPort (for instance, mistakenly connecting to a ControlPort
|
||||
# instead) we'll likely fail during SSL negotiation. This can result
|
||||
# in a variety of responses so normalizing what we can...
|
||||
#
|
||||
# Debian 9.5: [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:661)
|
||||
# Ubuntu 16.04: [SSL: UNKNOWN_PROTOCOL] unknown protocol (_ssl.c:590)
|
||||
# Ubuntu 12.04: [Errno 1] _ssl.c:504: error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol
|
||||
|
||||
if 'unknown protocol' in str(exc) or 'wrong version number' in str(exc):
|
||||
raise stem.SocketError("Failed to SSL authenticate to %s:%i. Maybe it isn't an ORPort?" % (address, port))
|
||||
|
||||
raise
|
||||
|
||||
# To negotiate our link protocol the first VERSIONS cell is expected to use
|
||||
# a circuit ID field size from protocol version 1-3 for backward
|
||||
# compatibility...
|
||||
#
|
||||
# The first VERSIONS cell, and any cells sent before the
|
||||
# first VERSIONS cell, always have CIRCID_LEN == 2 for backward
|
||||
# compatibility.
|
||||
|
||||
conn.send(stem.client.cell.VersionsCell(link_protocols).pack(2))
|
||||
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)))
|
||||
|
||||
# Establishing connections requires sending a NETINFO, but including our
|
||||
# address is optional. We can revisit including it when we have a usecase
|
||||
# where it would help.
|
||||
|
||||
link_protocol = max(common_protocols)
|
||||
conn.send(stem.client.cell.NetinfoCell(relay_addr, []).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.
|
||||
"""
|
||||
|
||||
with self._orport_lock:
|
||||
return self._orport.close()
|
||||
|
||||
def create_circuit(self):
|
||||
"""
|
||||
Establishes a new circuit.
|
||||
"""
|
||||
|
||||
with self._orport_lock:
|
||||
circ_id = max(self._circuits) + 1 if self._circuits else self.link_protocol.first_circ_id
|
||||
|
||||
create_fast_cell = stem.client.cell.CreateFastCell(circ_id)
|
||||
self._orport.send(create_fast_cell.pack(self.link_protocol))
|
||||
|
||||
response = stem.client.cell.Cell.unpack(self._orport.recv(), self.link_protocol)
|
||||
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 = list(created_fast_cells)[0]
|
||||
kdf = KDF.from_value(create_fast_cell.key_material + created_fast_cell.key_material)
|
||||
|
||||
if created_fast_cell.derivative_key != kdf.key_hash:
|
||||
raise ValueError('Remote failed to prove that it knows our shared key')
|
||||
|
||||
circ = Circuit(self, circ_id, kdf)
|
||||
self._circuits[circ.id] = circ
|
||||
|
||||
return circ
|
||||
|
||||
def __iter__(self):
|
||||
with self._orport_lock:
|
||||
for circ in self._circuits.values():
|
||||
yield circ
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exit_type, value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
class Circuit(object):
|
||||
"""
|
||||
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.client.Relay relay: relay 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
|
||||
"""
|
||||
|
||||
def __init__(self, relay, circ_id, kdf):
|
||||
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
|
||||
|
||||
ctr = modes.CTR(ZERO * (algorithms.AES.block_size // 8))
|
||||
|
||||
self.relay = relay
|
||||
self.id = circ_id
|
||||
self.forward_digest = hashlib.sha1(kdf.forward_digest)
|
||||
self.backward_digest = hashlib.sha1(kdf.backward_digest)
|
||||
self.forward_key = Cipher(algorithms.AES(kdf.forward_key), ctr, default_backend()).encryptor()
|
||||
self.backward_key = Cipher(algorithms.AES(kdf.backward_key), ctr, default_backend()).decryptor()
|
||||
|
||||
def send(self, command, data = '', stream_id = 0):
|
||||
"""
|
||||
Sends a message over the circuit.
|
||||
|
||||
:param stem.client.datatype.RelayCommand command: command to be issued
|
||||
:param bytes data: message payload
|
||||
:param int stream_id: specific stream this concerns
|
||||
|
||||
:returns: **list** of :class:`~stem.client.cell.RelayCell` responses
|
||||
"""
|
||||
|
||||
with self.relay._orport_lock:
|
||||
# Encrypt and send the cell. Our digest/key only updates if the cell is
|
||||
# successfully sent.
|
||||
|
||||
cell = stem.client.cell.RelayCell(self.id, command, data, stream_id = stream_id)
|
||||
payload, forward_key, forward_digest = cell.encrypt(self.relay.link_protocol, self.forward_key, self.forward_digest)
|
||||
self.relay._orport.send(payload)
|
||||
|
||||
self.forward_digest = forward_digest
|
||||
self.forward_key = forward_key
|
||||
|
||||
# Decrypt relay cells received in response. Again, our digest/key only
|
||||
# updates when handled successfully.
|
||||
|
||||
reply = self.relay._orport.recv()
|
||||
reply_cells = []
|
||||
|
||||
if len(reply) % self.relay.link_protocol.fixed_cell_length != 0:
|
||||
raise stem.ProtocolError('Circuit response should be a series of RELAY cells, but received an unexpected size for a response: %i' % len(reply))
|
||||
|
||||
while reply:
|
||||
encrypted_cell, reply = split(reply, self.relay.link_protocol.fixed_cell_length)
|
||||
decrypted_cell, backward_key, backward_digest = stem.client.cell.RelayCell.decrypt(self.relay.link_protocol, encrypted_cell, self.backward_key, self.backward_digest)
|
||||
|
||||
if self.id != decrypted_cell.circ_id:
|
||||
raise stem.ProtocolError('Response should be for circuit id %i, not %i' % (self.id, decrypted_cell.circ_id))
|
||||
|
||||
self.backward_digest = backward_digest
|
||||
self.backward_key = backward_key
|
||||
|
||||
reply_cells.append(decrypted_cell)
|
||||
|
||||
return reply_cells
|
||||
|
||||
def close(self):
|
||||
with self.relay._orport_lock:
|
||||
self.relay._orport.send(stem.client.cell.DestroyCell(self.id).pack(self.relay.link_protocol))
|
||||
del self.relay._circuits[self.id]
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exit_type, value, traceback):
|
||||
self.close()
|
||||
859
Shared/lib/python3.7/site-packages/stem/client/cell.py
Normal file
859
Shared/lib/python3.7/site-packages/stem/client/cell.py
Normal file
|
|
@ -0,0 +1,859 @@
|
|||
# Copyright 2018, Damian Johnson and The Tor Project
|
||||
# See LICENSE for licensing information
|
||||
|
||||
"""
|
||||
Messages communicated over a Tor relay's ORPort.
|
||||
|
||||
.. versionadded:: 1.7.0
|
||||
|
||||
**Module Overview:**
|
||||
|
||||
::
|
||||
|
||||
Cell - Base class for ORPort messages.
|
||||
|- CircuitCell - Circuit management.
|
||||
| |- CreateCell - Create a circuit. (section 5.1)
|
||||
| |- CreatedCell - Acknowledge create. (section 5.1)
|
||||
| |- RelayCell - End-to-end data. (section 6.1)
|
||||
| |- DestroyCell - Stop using a circuit. (section 5.4)
|
||||
| |- CreateFastCell - Create a circuit, no PK. (section 5.1)
|
||||
| |- CreatedFastCell - Circuit created, no PK. (section 5.1)
|
||||
| |- RelayEarlyCell - End-to-end data; limited. (section 5.6)
|
||||
| |- Create2Cell - Extended CREATE cell. (section 5.1)
|
||||
| +- Created2Cell - Extended CREATED cell. (section 5.1)
|
||||
|
|
||||
|- PaddingCell - Padding negotiation. (section 7.2)
|
||||
|- VersionsCell - Negotiate proto version. (section 4)
|
||||
|- NetinfoCell - Time and address info. (section 4.5)
|
||||
|- PaddingNegotiateCell - Padding negotiation. (section 7.2)
|
||||
|- VPaddingCell - Variable-length padding. (section 7.2)
|
||||
|- CertsCell - Relay certificates. (section 4.2)
|
||||
|- AuthChallengeCell - Challenge value. (section 4.3)
|
||||
|- AuthenticateCell - Client authentication. (section 4.5)
|
||||
|- AuthorizeCell - Client authorization. (not yet used)
|
||||
|
|
||||
|- pack - encodes cell into bytes
|
||||
|- unpack - decodes series of cells
|
||||
+- pop - decodes cell with remainder
|
||||
"""
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
|
||||
import stem.util
|
||||
|
||||
from stem import UNDEFINED
|
||||
from stem.client.datatype import HASH_LEN, ZERO, LinkProtocol, Address, Certificate, CloseReason, RelayCommand, Size, split
|
||||
from stem.util import datetime_to_unix, str_tools
|
||||
|
||||
FIXED_PAYLOAD_LEN = 509 # PAYLOAD_LEN, per tor-spec section 0.2
|
||||
AUTH_CHALLENGE_SIZE = 32
|
||||
RELAY_DIGEST_SIZE = Size.LONG
|
||||
|
||||
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):
|
||||
"""
|
||||
Metadata for ORPort cells.
|
||||
|
||||
Unused padding are **not** used in equality checks or hashing. If two cells
|
||||
differ only in their *unused* attribute they are functionally equal.
|
||||
|
||||
The following cell types explicitly don't have *unused* content:
|
||||
* PaddingCell (we consider all content part of payload)
|
||||
* VersionsCell (all content is unpacked and treated as a version specification)
|
||||
* VPaddingCell (we consider all content part of payload)
|
||||
|
||||
:var bytes unused: unused filler that padded the cell to the expected size
|
||||
"""
|
||||
|
||||
NAME = 'UNKNOWN'
|
||||
VALUE = -1
|
||||
IS_FIXED_SIZE = False
|
||||
|
||||
def __init__(self, unused = b''):
|
||||
super(Cell, self).__init__()
|
||||
self.unused = unused
|
||||
|
||||
@staticmethod
|
||||
def by_name(name):
|
||||
"""
|
||||
Provides cell attributes by its name.
|
||||
|
||||
:param str name: cell command to fetch
|
||||
|
||||
:raises: **ValueError** if cell type is invalid
|
||||
"""
|
||||
|
||||
for _, cls in inspect.getmembers(sys.modules[__name__]):
|
||||
if name == getattr(cls, 'NAME', UNDEFINED):
|
||||
return cls
|
||||
|
||||
raise ValueError("'%s' isn't a valid cell type" % name)
|
||||
|
||||
@staticmethod
|
||||
def by_value(value):
|
||||
"""
|
||||
Provides cell attributes by its value.
|
||||
|
||||
:param int value: cell value to fetch
|
||||
|
||||
:raises: **ValueError** if cell type is invalid
|
||||
"""
|
||||
|
||||
for _, cls in inspect.getmembers(sys.modules[__name__]):
|
||||
if value == getattr(cls, 'VALUE', UNDEFINED):
|
||||
return cls
|
||||
|
||||
raise ValueError("'%s' isn't a valid cell value" % value)
|
||||
|
||||
def pack(self, link_protocol):
|
||||
raise NotImplementedError('Packing not yet implemented for %s cells' % type(self).NAME)
|
||||
|
||||
@staticmethod
|
||||
def unpack(content, link_protocol):
|
||||
"""
|
||||
Unpacks all cells from a response.
|
||||
|
||||
:param bytes content: payload to decode
|
||||
:param int link_protocol: link protocol version
|
||||
|
||||
:returns: :class:`~stem.client.cell.Cell` generator
|
||||
|
||||
:raises:
|
||||
* ValueError if content is malformed
|
||||
* NotImplementedError if unable to unpack any of the cell types
|
||||
"""
|
||||
|
||||
while content:
|
||||
cell, content = Cell.pop(content, link_protocol)
|
||||
yield cell
|
||||
|
||||
@staticmethod
|
||||
def pop(content, link_protocol):
|
||||
"""
|
||||
Unpacks the first cell.
|
||||
|
||||
:param bytes content: payload to decode
|
||||
:param int link_protocol: link protocol version
|
||||
|
||||
:returns: (:class:`~stem.client.cell.Cell`, remainder) tuple
|
||||
|
||||
:raises:
|
||||
* ValueError if content is malformed
|
||||
* NotImplementedError if unable to unpack this cell type
|
||||
"""
|
||||
|
||||
link_protocol = LinkProtocol(link_protocol)
|
||||
|
||||
circ_id, content = link_protocol.circ_id_size.pop(content)
|
||||
command, content = Size.CHAR.pop(content)
|
||||
cls = Cell.by_value(command)
|
||||
|
||||
if cls.IS_FIXED_SIZE:
|
||||
payload_len = FIXED_PAYLOAD_LEN
|
||||
else:
|
||||
payload_len, content = Size.SHORT.pop(content)
|
||||
|
||||
if len(content) < payload_len:
|
||||
raise ValueError('%s cell should have a payload of %i bytes, but only had %i' % (cls.NAME, payload_len, len(content)))
|
||||
|
||||
payload, content = split(content, payload_len)
|
||||
return cls._unpack(payload, circ_id, link_protocol), content
|
||||
|
||||
@classmethod
|
||||
def _pack(cls, link_protocol, payload, unused = b'', circ_id = None):
|
||||
"""
|
||||
Provides bytes that can be used on the wire for these cell attributes.
|
||||
Format of a properly packed cell depends on if it's fixed or variable
|
||||
sized...
|
||||
|
||||
::
|
||||
|
||||
Fixed: [ CircuitID ][ Command ][ Payload ][ Padding ]
|
||||
Variable: [ CircuitID ][ Command ][ Size ][ Payload ]
|
||||
|
||||
:param str name: cell command
|
||||
:param int link_protocol: link protocol version
|
||||
:param bytes payload: cell payload
|
||||
:param int circ_id: circuit id, if a CircuitCell
|
||||
|
||||
:returns: **bytes** with the encoded payload
|
||||
|
||||
:raises: **ValueError** if cell type invalid or payload makes cell too large
|
||||
"""
|
||||
|
||||
if issubclass(cls, CircuitCell):
|
||||
if circ_id is None:
|
||||
raise ValueError('%s cells require a circuit identifier' % cls.NAME)
|
||||
elif circ_id < 1:
|
||||
raise ValueError('Circuit identifiers must a positive integer, not %s' % circ_id)
|
||||
else:
|
||||
if circ_id is not None:
|
||||
raise ValueError('%s cells should not specify a circuit identifier' % cls.NAME)
|
||||
|
||||
circ_id = 0 # cell doesn't concern a circuit, default field to zero
|
||||
|
||||
link_protocol = LinkProtocol(link_protocol)
|
||||
|
||||
cell = bytearray()
|
||||
cell += link_protocol.circ_id_size.pack(circ_id)
|
||||
cell += Size.CHAR.pack(cls.VALUE)
|
||||
cell += b'' if cls.IS_FIXED_SIZE else Size.SHORT.pack(len(payload) + len(unused))
|
||||
cell += payload
|
||||
|
||||
# include the unused portion (typically from unpacking)
|
||||
cell += unused
|
||||
|
||||
# pad fixed sized cells to the required length
|
||||
|
||||
if cls.IS_FIXED_SIZE:
|
||||
if len(cell) > link_protocol.fixed_cell_length:
|
||||
raise ValueError('Cell of type %s is too large (%i bytes), must not be more than %i. Check payload size (was %i bytes)' % (cls.NAME, len(cell), link_protocol.fixed_cell_length, len(payload)))
|
||||
|
||||
cell += ZERO * (link_protocol.fixed_cell_length - len(cell))
|
||||
|
||||
return bytes(cell)
|
||||
|
||||
@classmethod
|
||||
def _unpack(cls, content, circ_id, link_protocol):
|
||||
"""
|
||||
Subclass implementation for unpacking cell content.
|
||||
|
||||
:param bytes content: payload to decode
|
||||
:param stem.client.datatype.LinkProtocol link_protocol: link protocol version
|
||||
:param int circ_id: circuit id cell is for
|
||||
|
||||
:returns: instance of this cell type
|
||||
|
||||
:raises: **ValueError** if content is malformed
|
||||
"""
|
||||
|
||||
raise NotImplementedError('Unpacking not yet implemented for %s cells' % cls.NAME)
|
||||
|
||||
def __eq__(self, other):
|
||||
return hash(self) == hash(other) if isinstance(other, Cell) else False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
|
||||
class CircuitCell(Cell):
|
||||
"""
|
||||
Cell concerning circuits.
|
||||
|
||||
:var int circ_id: circuit id
|
||||
"""
|
||||
|
||||
def __init__(self, circ_id, unused = b''):
|
||||
super(CircuitCell, self).__init__(unused)
|
||||
self.circ_id = circ_id
|
||||
|
||||
|
||||
class PaddingCell(Cell):
|
||||
"""
|
||||
Randomized content to either keep activity going on a circuit.
|
||||
|
||||
:var bytes payload: randomized payload
|
||||
"""
|
||||
|
||||
NAME = 'PADDING'
|
||||
VALUE = 0
|
||||
IS_FIXED_SIZE = True
|
||||
|
||||
def __init__(self, payload = None):
|
||||
if not payload:
|
||||
payload = os.urandom(FIXED_PAYLOAD_LEN)
|
||||
elif len(payload) != FIXED_PAYLOAD_LEN:
|
||||
raise ValueError('Padding payload should be %i bytes, but was %i' % (FIXED_PAYLOAD_LEN, len(payload)))
|
||||
|
||||
super(PaddingCell, self).__init__()
|
||||
self.payload = payload
|
||||
|
||||
def pack(self, link_protocol):
|
||||
return PaddingCell._pack(link_protocol, self.payload)
|
||||
|
||||
@classmethod
|
||||
def _unpack(cls, content, circ_id, link_protocol):
|
||||
return PaddingCell(content)
|
||||
|
||||
def __hash__(self):
|
||||
return stem.util._hash_attr(self, 'payload', cache = True)
|
||||
|
||||
|
||||
class CreateCell(CircuitCell):
|
||||
NAME = 'CREATE'
|
||||
VALUE = 1
|
||||
IS_FIXED_SIZE = True
|
||||
|
||||
def __init__(self):
|
||||
super(CreateCell, self).__init__() # TODO: implement
|
||||
|
||||
|
||||
class CreatedCell(CircuitCell):
|
||||
NAME = 'CREATED'
|
||||
VALUE = 2
|
||||
IS_FIXED_SIZE = True
|
||||
|
||||
def __init__(self):
|
||||
super(CreatedCell, self).__init__() # TODO: implement
|
||||
|
||||
|
||||
class RelayCell(CircuitCell):
|
||||
"""
|
||||
Command concerning a relay circuit.
|
||||
|
||||
Our 'recognized' attribute provides a cheap (but incomplete) check for if our
|
||||
cell payload is encrypted. If non-zero our payload *IS* encrypted, but if
|
||||
zero we're *PROBABLY* fully decrypted. This uncertainty is because encrypted
|
||||
cells have a small chance of coincidently producing zero for this value as
|
||||
well.
|
||||
|
||||
:var stem.client.RelayCommand command: command to be issued
|
||||
:var int command_int: integer value of our command
|
||||
:var bytes data: payload of the cell
|
||||
:var int recognized: non-zero if payload is encrypted
|
||||
:var int digest: running digest held with the relay
|
||||
:var int stream_id: specific stream this concerns
|
||||
"""
|
||||
|
||||
NAME = 'RELAY'
|
||||
VALUE = 3
|
||||
IS_FIXED_SIZE = True
|
||||
|
||||
def __init__(self, circ_id, command, data, digest = 0, stream_id = 0, recognized = 0, unused = b''):
|
||||
if 'HASH' in str(type(digest)):
|
||||
# Unfortunately hashlib generates from a dynamic private class so
|
||||
# isinstance() isn't such a great option. With python2/python3 the
|
||||
# name is 'hashlib.HASH' whereas PyPy calls it just 'HASH'.
|
||||
|
||||
digest_packed = digest.digest()[:RELAY_DIGEST_SIZE.size]
|
||||
digest = RELAY_DIGEST_SIZE.unpack(digest_packed)
|
||||
elif stem.util._is_str(digest):
|
||||
digest_packed = digest[:RELAY_DIGEST_SIZE.size]
|
||||
digest = RELAY_DIGEST_SIZE.unpack(digest_packed)
|
||||
elif stem.util._is_int(digest):
|
||||
pass
|
||||
else:
|
||||
raise ValueError('RELAY cell digest must be a hash, string, or int but was a %s' % type(digest).__name__)
|
||||
|
||||
super(RelayCell, self).__init__(circ_id, unused)
|
||||
self.command, self.command_int = RelayCommand.get(command)
|
||||
self.recognized = recognized
|
||||
self.stream_id = stream_id
|
||||
self.digest = digest
|
||||
self.data = str_tools._to_bytes(data)
|
||||
|
||||
if digest == 0:
|
||||
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 STREAM_ID_DISALLOWED:
|
||||
raise ValueError('%s relay cells concern the circuit itself and cannot have a stream id' % self.command)
|
||||
|
||||
def pack(self, link_protocol):
|
||||
payload = bytearray()
|
||||
payload += Size.CHAR.pack(self.command_int)
|
||||
payload += Size.SHORT.pack(self.recognized)
|
||||
payload += Size.SHORT.pack(self.stream_id)
|
||||
payload += Size.LONG.pack(self.digest)
|
||||
payload += Size.SHORT.pack(len(self.data))
|
||||
payload += self.data
|
||||
|
||||
return RelayCell._pack(link_protocol, bytes(payload), self.unused, self.circ_id)
|
||||
|
||||
@staticmethod
|
||||
def decrypt(link_protocol, content, key, digest):
|
||||
"""
|
||||
Decrypts content as a relay cell addressed to us. This provides back a
|
||||
tuple of the form...
|
||||
|
||||
::
|
||||
|
||||
(cell (RelayCell), new_key (CipherContext), new_digest (HASH))
|
||||
|
||||
:param int link_protocol: link protocol version
|
||||
:param bytes content: cell content to be decrypted
|
||||
:param cryptography.hazmat.primitives.ciphers.CipherContext key:
|
||||
key established with the relay we received this cell from
|
||||
:param HASH digest: running digest held with the relay
|
||||
|
||||
:returns: **tuple** with our decrypted cell and updated key/digest
|
||||
|
||||
:raises: :class:`stem.ProtocolError` if content doesn't belong to a relay
|
||||
cell
|
||||
"""
|
||||
|
||||
new_key = copy.copy(key)
|
||||
new_digest = digest.copy()
|
||||
|
||||
if len(content) != link_protocol.fixed_cell_length:
|
||||
raise stem.ProtocolError('RELAY cells should be %i bytes, but received %i' % (link_protocol.fixed_cell_length, len(content)))
|
||||
|
||||
circ_id, content = link_protocol.circ_id_size.pop(content)
|
||||
command, encrypted_payload = Size.CHAR.pop(content)
|
||||
|
||||
if command != RelayCell.VALUE:
|
||||
raise stem.ProtocolError('Cannot decrypt as a RELAY cell. This had command %i instead.' % command)
|
||||
|
||||
payload = new_key.update(encrypted_payload)
|
||||
|
||||
cell = RelayCell._unpack(payload, circ_id, link_protocol)
|
||||
|
||||
# TODO: Implement our decryption digest. It is used to support relaying
|
||||
# within multi-hop circuits. On first glance this should go something
|
||||
# like...
|
||||
#
|
||||
# # Our updated digest is calculated based on this cell with a blanked
|
||||
# # digest field.
|
||||
#
|
||||
# digest_cell = RelayCell(self.circ_id, self.command, self.data, 0, self.stream_id, self.recognized, self.unused)
|
||||
# new_digest.update(digest_cell.pack(link_protocol))
|
||||
#
|
||||
# is_encrypted == cell.recognized != 0 or self.digest == new_digest
|
||||
#
|
||||
# ... or something like that. Until we attempt to support relaying this is
|
||||
# both moot and difficult to exercise in order to ensure we get it right.
|
||||
|
||||
return cell, new_key, new_digest
|
||||
|
||||
def encrypt(self, link_protocol, key, digest):
|
||||
"""
|
||||
Encrypts our cell content to be sent with the given key. This provides back
|
||||
a tuple of the form...
|
||||
|
||||
::
|
||||
|
||||
(payload (bytes), new_key (CipherContext), new_digest (HASH))
|
||||
|
||||
:param int link_protocol: link protocol version
|
||||
:param cryptography.hazmat.primitives.ciphers.CipherContext key:
|
||||
key established with the relay we're sending this cell to
|
||||
:param HASH digest: running digest held with the relay
|
||||
|
||||
:returns: **tuple** with our encrypted payload and updated key/digest
|
||||
"""
|
||||
|
||||
new_key = copy.copy(key)
|
||||
new_digest = digest.copy()
|
||||
|
||||
# Digests are computed from our payload, not including our header's circuit
|
||||
# id (2 or 4 bytes) and command (1 byte).
|
||||
|
||||
header_size = link_protocol.circ_id_size.size + 1
|
||||
payload_without_digest = self.pack(link_protocol)[header_size:]
|
||||
new_digest.update(payload_without_digest)
|
||||
|
||||
# Pack a copy of ourselves with our newly calculated digest, and encrypt
|
||||
# the payload. Header remains plaintext.
|
||||
|
||||
cell = RelayCell(self.circ_id, self.command, self.data, new_digest, self.stream_id, self.recognized, self.unused)
|
||||
header, payload = split(cell.pack(link_protocol), header_size)
|
||||
|
||||
return header + new_key.update(payload), new_key, new_digest
|
||||
|
||||
@classmethod
|
||||
def _unpack(cls, content, circ_id, link_protocol):
|
||||
command, content = Size.CHAR.pop(content)
|
||||
recognized, content = Size.SHORT.pop(content) # 'recognized' field
|
||||
stream_id, content = Size.SHORT.pop(content)
|
||||
digest, content = Size.LONG.pop(content)
|
||||
data_len, content = Size.SHORT.pop(content)
|
||||
data, unused = split(content, data_len)
|
||||
|
||||
if len(data) != data_len:
|
||||
raise ValueError('%s cell said it had %i bytes of data, but only had %i' % (cls.NAME, data_len, len(data)))
|
||||
|
||||
return RelayCell(circ_id, command, data, digest, stream_id, recognized, unused)
|
||||
|
||||
def __hash__(self):
|
||||
return stem.util._hash_attr(self, 'command_int', 'stream_id', 'digest', 'data', cache = True)
|
||||
|
||||
|
||||
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
|
||||
|
||||
def __init__(self, circ_id, reason = CloseReason.NONE, unused = b''):
|
||||
super(DestroyCell, self).__init__(circ_id, unused)
|
||||
self.reason, self.reason_int = CloseReason.get(reason)
|
||||
|
||||
def pack(self, link_protocol):
|
||||
return DestroyCell._pack(link_protocol, Size.CHAR.pack(self.reason_int), self.unused, self.circ_id)
|
||||
|
||||
@classmethod
|
||||
def _unpack(cls, content, circ_id, link_protocol):
|
||||
reason, unused = Size.CHAR.pop(content)
|
||||
return DestroyCell(circ_id, reason, unused)
|
||||
|
||||
def __hash__(self):
|
||||
return stem.util._hash_attr(self, 'circ_id', 'reason_int', cache = True)
|
||||
|
||||
|
||||
class CreateFastCell(CircuitCell):
|
||||
"""
|
||||
Create a circuit with our first hop. This is lighter weight than further hops
|
||||
because we've already established the relay's identity and secret key.
|
||||
|
||||
:var bytes key_material: randomized key material
|
||||
"""
|
||||
|
||||
NAME = 'CREATE_FAST'
|
||||
VALUE = 5
|
||||
IS_FIXED_SIZE = True
|
||||
|
||||
def __init__(self, circ_id, key_material = None, unused = b''):
|
||||
if not key_material:
|
||||
key_material = os.urandom(HASH_LEN)
|
||||
elif len(key_material) != HASH_LEN:
|
||||
raise ValueError('Key material should be %i bytes, but was %i' % (HASH_LEN, len(key_material)))
|
||||
|
||||
super(CreateFastCell, self).__init__(circ_id, unused)
|
||||
self.key_material = key_material
|
||||
|
||||
def pack(self, link_protocol):
|
||||
return CreateFastCell._pack(link_protocol, self.key_material, self.unused, self.circ_id)
|
||||
|
||||
@classmethod
|
||||
def _unpack(cls, content, circ_id, link_protocol):
|
||||
key_material, unused = split(content, HASH_LEN)
|
||||
|
||||
if len(key_material) != HASH_LEN:
|
||||
raise ValueError('Key material should be %i bytes, but was %i' % (HASH_LEN, len(key_material)))
|
||||
|
||||
return CreateFastCell(circ_id, key_material, unused)
|
||||
|
||||
def __hash__(self):
|
||||
return stem.util._hash_attr(self, 'circ_id', 'key_material', cache = True)
|
||||
|
||||
|
||||
class CreatedFastCell(CircuitCell):
|
||||
"""
|
||||
CREATE_FAST reply.
|
||||
|
||||
:var bytes key_material: randomized key material
|
||||
:var bytes derivative_key: hash proving the relay knows our shared key
|
||||
"""
|
||||
|
||||
NAME = 'CREATED_FAST'
|
||||
VALUE = 6
|
||||
IS_FIXED_SIZE = True
|
||||
|
||||
def __init__(self, circ_id, derivative_key, key_material = None, unused = b''):
|
||||
if not key_material:
|
||||
key_material = os.urandom(HASH_LEN)
|
||||
elif len(key_material) != HASH_LEN:
|
||||
raise ValueError('Key material should be %i bytes, but was %i' % (HASH_LEN, len(key_material)))
|
||||
|
||||
if len(derivative_key) != HASH_LEN:
|
||||
raise ValueError('Derivatived key should be %i bytes, but was %i' % (HASH_LEN, len(derivative_key)))
|
||||
|
||||
super(CreatedFastCell, self).__init__(circ_id, unused)
|
||||
self.key_material = key_material
|
||||
self.derivative_key = derivative_key
|
||||
|
||||
def pack(self, link_protocol):
|
||||
return CreatedFastCell._pack(link_protocol, self.key_material + self.derivative_key, self.unused, self.circ_id)
|
||||
|
||||
@classmethod
|
||||
def _unpack(cls, content, circ_id, link_protocol):
|
||||
if len(content) < HASH_LEN * 2:
|
||||
raise ValueError('Key material and derivatived key should be %i bytes, but was %i' % (HASH_LEN * 2, len(content)))
|
||||
|
||||
key_material, content = split(content, HASH_LEN)
|
||||
derivative_key, content = split(content, HASH_LEN)
|
||||
|
||||
return CreatedFastCell(circ_id, derivative_key, key_material, content)
|
||||
|
||||
def __hash__(self):
|
||||
return stem.util._hash_attr(self, 'circ_id', 'derivative_key', 'key_material', cache = True)
|
||||
|
||||
|
||||
class VersionsCell(Cell):
|
||||
"""
|
||||
Link version negotiation cell.
|
||||
|
||||
:var list versions: link versions
|
||||
"""
|
||||
|
||||
NAME = 'VERSIONS'
|
||||
VALUE = 7
|
||||
IS_FIXED_SIZE = False
|
||||
|
||||
def __init__(self, versions):
|
||||
super(VersionsCell, self).__init__()
|
||||
self.versions = versions
|
||||
|
||||
def pack(self, link_protocol):
|
||||
payload = b''.join([Size.SHORT.pack(v) for v in self.versions])
|
||||
return VersionsCell._pack(link_protocol, payload)
|
||||
|
||||
@classmethod
|
||||
def _unpack(cls, content, circ_id, link_protocol):
|
||||
link_protocols = []
|
||||
|
||||
while content:
|
||||
version, content = Size.SHORT.pop(content)
|
||||
link_protocols.append(version)
|
||||
|
||||
return VersionsCell(link_protocols)
|
||||
|
||||
def __hash__(self):
|
||||
return stem.util._hash_attr(self, 'versions', cache = True)
|
||||
|
||||
|
||||
class NetinfoCell(Cell):
|
||||
"""
|
||||
Information relays exchange about each other.
|
||||
|
||||
:var datetime timestamp: current time
|
||||
:var stem.client.Address receiver_address: receiver's OR address
|
||||
:var list sender_addresses: sender's OR addresses
|
||||
"""
|
||||
|
||||
NAME = 'NETINFO'
|
||||
VALUE = 8
|
||||
IS_FIXED_SIZE = True
|
||||
|
||||
def __init__(self, receiver_address, sender_addresses, timestamp = None, unused = b''):
|
||||
super(NetinfoCell, self).__init__(unused)
|
||||
self.timestamp = timestamp if timestamp else datetime.datetime.now()
|
||||
self.receiver_address = receiver_address
|
||||
self.sender_addresses = sender_addresses
|
||||
|
||||
def pack(self, link_protocol):
|
||||
payload = bytearray()
|
||||
payload += Size.LONG.pack(int(datetime_to_unix(self.timestamp)))
|
||||
payload += self.receiver_address.pack()
|
||||
payload += Size.CHAR.pack(len(self.sender_addresses))
|
||||
|
||||
for addr in self.sender_addresses:
|
||||
payload += addr.pack()
|
||||
|
||||
return NetinfoCell._pack(link_protocol, bytes(payload), self.unused)
|
||||
|
||||
@classmethod
|
||||
def _unpack(cls, content, circ_id, link_protocol):
|
||||
timestamp, content = Size.LONG.pop(content)
|
||||
receiver_address, content = Address.pop(content)
|
||||
|
||||
sender_addresses = []
|
||||
sender_addr_count, content = Size.CHAR.pop(content)
|
||||
|
||||
for i in range(sender_addr_count):
|
||||
addr, content = Address.pop(content)
|
||||
sender_addresses.append(addr)
|
||||
|
||||
return NetinfoCell(receiver_address, sender_addresses, datetime.datetime.utcfromtimestamp(timestamp), unused = content)
|
||||
|
||||
def __hash__(self):
|
||||
return stem.util._hash_attr(self, 'timestamp', 'receiver_address', 'sender_addresses', cache = True)
|
||||
|
||||
|
||||
class RelayEarlyCell(CircuitCell):
|
||||
NAME = 'RELAY_EARLY'
|
||||
VALUE = 9
|
||||
IS_FIXED_SIZE = True
|
||||
|
||||
def __init__(self):
|
||||
super(RelayEarlyCell, self).__init__() # TODO: implement
|
||||
|
||||
|
||||
class Create2Cell(CircuitCell):
|
||||
NAME = 'CREATE2'
|
||||
VALUE = 10
|
||||
IS_FIXED_SIZE = True
|
||||
|
||||
def __init__(self):
|
||||
super(Create2Cell, self).__init__() # TODO: implement
|
||||
|
||||
|
||||
class Created2Cell(Cell):
|
||||
NAME = 'CREATED2'
|
||||
VALUE = 11
|
||||
IS_FIXED_SIZE = True
|
||||
|
||||
def __init__(self):
|
||||
super(Created2Cell, self).__init__() # TODO: implement
|
||||
|
||||
|
||||
class PaddingNegotiateCell(Cell):
|
||||
NAME = 'PADDING_NEGOTIATE'
|
||||
VALUE = 12
|
||||
IS_FIXED_SIZE = True
|
||||
|
||||
def __init__(self):
|
||||
super(PaddingNegotiateCell, self).__init__() # TODO: implement
|
||||
|
||||
|
||||
class VPaddingCell(Cell):
|
||||
"""
|
||||
Variable length randomized content to either keep activity going on a circuit.
|
||||
|
||||
:var bytes payload: randomized payload
|
||||
"""
|
||||
|
||||
NAME = 'VPADDING'
|
||||
VALUE = 128
|
||||
IS_FIXED_SIZE = False
|
||||
|
||||
def __init__(self, size = None, payload = None):
|
||||
if size is None and payload is None:
|
||||
raise ValueError('VPaddingCell constructor must specify payload or size')
|
||||
elif size is not None and size < 0:
|
||||
raise ValueError('VPaddingCell size (%s) cannot be negative' % size)
|
||||
elif size is not None and payload is not None and size != len(payload):
|
||||
raise ValueError('VPaddingCell constructor specified both a size of %i bytes and payload of %i bytes' % (size, len(payload)))
|
||||
|
||||
super(VPaddingCell, self).__init__()
|
||||
self.payload = payload if payload is not None else os.urandom(size)
|
||||
|
||||
def pack(self, link_protocol):
|
||||
return VPaddingCell._pack(link_protocol, self.payload)
|
||||
|
||||
@classmethod
|
||||
def _unpack(cls, content, circ_id, link_protocol):
|
||||
return VPaddingCell(payload = content)
|
||||
|
||||
def __hash__(self):
|
||||
return stem.util._hash_attr(self, 'payload', cache = True)
|
||||
|
||||
|
||||
class CertsCell(Cell):
|
||||
"""
|
||||
Certificate held by the relay we're communicating with.
|
||||
|
||||
:var list certificates: :class:`~stem.client.Certificate` of the relay
|
||||
"""
|
||||
|
||||
NAME = 'CERTS'
|
||||
VALUE = 129
|
||||
IS_FIXED_SIZE = False
|
||||
|
||||
def __init__(self, certs, unused = b''):
|
||||
super(CertsCell, self).__init__(unused)
|
||||
self.certificates = certs
|
||||
|
||||
def pack(self, link_protocol):
|
||||
return CertsCell._pack(link_protocol, Size.CHAR.pack(len(self.certificates)) + b''.join([cert.pack() for cert in self.certificates]), self.unused)
|
||||
|
||||
@classmethod
|
||||
def _unpack(cls, content, circ_id, link_protocol):
|
||||
cert_count, content = Size.CHAR.pop(content)
|
||||
certs = []
|
||||
|
||||
for i in range(cert_count):
|
||||
if not content:
|
||||
raise ValueError('CERTS cell indicates it should have %i certificates, but only contained %i' % (cert_count, len(certs)))
|
||||
|
||||
cert, content = Certificate.pop(content)
|
||||
certs.append(cert)
|
||||
|
||||
return CertsCell(certs, unused = content)
|
||||
|
||||
def __hash__(self):
|
||||
return stem.util._hash_attr(self, 'certificates', cache = True)
|
||||
|
||||
|
||||
class AuthChallengeCell(Cell):
|
||||
"""
|
||||
First step of the authentication handshake.
|
||||
|
||||
:var bytes challenge: random bytes for us to sign to authenticate
|
||||
:var list methods: authentication methods supported by the relay we're
|
||||
communicating with
|
||||
"""
|
||||
|
||||
NAME = 'AUTH_CHALLENGE'
|
||||
VALUE = 130
|
||||
IS_FIXED_SIZE = False
|
||||
|
||||
def __init__(self, methods, challenge = None, unused = b''):
|
||||
if not challenge:
|
||||
challenge = os.urandom(AUTH_CHALLENGE_SIZE)
|
||||
elif len(challenge) != AUTH_CHALLENGE_SIZE:
|
||||
raise ValueError('AUTH_CHALLENGE must be %i bytes, but was %i' % (AUTH_CHALLENGE_SIZE, len(challenge)))
|
||||
|
||||
super(AuthChallengeCell, self).__init__(unused)
|
||||
self.challenge = challenge
|
||||
self.methods = methods
|
||||
|
||||
def pack(self, link_protocol):
|
||||
payload = bytearray()
|
||||
payload += self.challenge
|
||||
payload += Size.SHORT.pack(len(self.methods))
|
||||
|
||||
for method in self.methods:
|
||||
payload += Size.SHORT.pack(method)
|
||||
|
||||
return AuthChallengeCell._pack(link_protocol, bytes(payload), self.unused)
|
||||
|
||||
@classmethod
|
||||
def _unpack(cls, content, circ_id, link_protocol):
|
||||
min_size = AUTH_CHALLENGE_SIZE + Size.SHORT.size
|
||||
if len(content) < min_size:
|
||||
raise ValueError('AUTH_CHALLENGE payload should be at least %i bytes, but was %i' % (min_size, len(content)))
|
||||
|
||||
challenge, content = split(content, AUTH_CHALLENGE_SIZE)
|
||||
method_count, content = Size.SHORT.pop(content)
|
||||
|
||||
if len(content) < method_count * Size.SHORT.size:
|
||||
raise ValueError('AUTH_CHALLENGE should have %i methods, but only had %i bytes for it' % (method_count, len(content)))
|
||||
|
||||
methods = []
|
||||
|
||||
for i in range(method_count):
|
||||
method, content = Size.SHORT.pop(content)
|
||||
methods.append(method)
|
||||
|
||||
return AuthChallengeCell(methods, challenge, unused = content)
|
||||
|
||||
def __hash__(self):
|
||||
return stem.util._hash_attr(self, 'challenge', 'methods', cache = True)
|
||||
|
||||
|
||||
class AuthenticateCell(Cell):
|
||||
NAME = 'AUTHENTICATE'
|
||||
VALUE = 131
|
||||
IS_FIXED_SIZE = False
|
||||
|
||||
def __init__(self):
|
||||
super(AuthenticateCell, self).__init__() # TODO: implement
|
||||
|
||||
|
||||
class AuthorizeCell(Cell):
|
||||
NAME = 'AUTHORIZE'
|
||||
VALUE = 132
|
||||
IS_FIXED_SIZE = False
|
||||
|
||||
def __init__(self):
|
||||
super(AuthorizeCell, self).__init__() # TODO: implement
|
||||
558
Shared/lib/python3.7/site-packages/stem/client/datatype.py
Normal file
558
Shared/lib/python3.7/site-packages/stem/client/datatype.py
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
# 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
|
||||
|
||||
LinkProtocol - ORPort protocol version.
|
||||
|
||||
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 struct
|
||||
|
||||
import stem.client.cell
|
||||
import stem.prereq
|
||||
import stem.util
|
||||
import stem.util.connection
|
||||
import stem.util.enum
|
||||
|
||||
ZERO = b'\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):
|
||||
"""
|
||||
Provides the (enum, int_value) tuple for a given value.
|
||||
"""
|
||||
|
||||
if stem.util._is_int(val):
|
||||
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 LinkProtocol(int):
|
||||
"""
|
||||
Constants that vary by our link protocol version.
|
||||
|
||||
:var int version: link protocol version
|
||||
:var stem.client.datatype.Size circ_id_size: circuit identifier field size
|
||||
:var int fixed_cell_length: size of cells with a fixed length
|
||||
:var int first_circ_id: When creating circuits we pick an unused identifier
|
||||
from a range that's determined by our link protocol.
|
||||
"""
|
||||
|
||||
def __new__(cls, version):
|
||||
if isinstance(version, LinkProtocol):
|
||||
return version # already a LinkProtocol
|
||||
|
||||
protocol = int.__new__(cls, version)
|
||||
protocol.version = version
|
||||
protocol.circ_id_size = Size.LONG if version > 3 else Size.SHORT
|
||||
protocol.first_circ_id = 0x80000000 if version > 3 else 0x01
|
||||
|
||||
cell_header_size = protocol.circ_id_size.size + 1 # circuit id (2 or 4 bytes) + command (1 byte)
|
||||
protocol.fixed_cell_length = cell_header_size + stem.client.cell.FIXED_PAYLOAD_LEN
|
||||
|
||||
return protocol
|
||||
|
||||
def __hash__(self):
|
||||
# All LinkProtocol attributes can be derived from our version, so that's
|
||||
# all we need in our hash. Offsetting by our type so we don't hash conflict
|
||||
# with ints.
|
||||
|
||||
return self.version * hash(str(type(self)))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, int):
|
||||
return self.version == other
|
||||
elif isinstance(other, LinkProtocol):
|
||||
return hash(self) == hash(other)
|
||||
else:
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __int__(self):
|
||||
return self.version
|
||||
|
||||
|
||||
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):
|
||||
# TODO: Python 2.6's struct module behaves a little differently in a couple
|
||||
# respsects...
|
||||
#
|
||||
# * Invalid types raise a TypeError rather than a struct.error.
|
||||
#
|
||||
# * Negative values are happily packed despite being unsigned fields with
|
||||
# a message printed to stdout (!) that says...
|
||||
#
|
||||
# stem/client/datatype.py:362: DeprecationWarning: struct integer overflow masking is deprecated
|
||||
# packed = struct.pack(self.format, content)
|
||||
# stem/client/datatype.py:362: DeprecationWarning: 'B' format requires 0 <= number <= 255
|
||||
# packed = struct.pack(self.format, content)
|
||||
#
|
||||
# Rather than adjust this method to account for these differences doing
|
||||
# duplicate upfront checks just for python 2.6. When we drop 2.6 support
|
||||
# this can obviously be dropped.
|
||||
|
||||
if stem.prereq._is_python_26():
|
||||
if not stem.util._is_int(content):
|
||||
raise ValueError('Size.pack encodes an integer, but was a %s' % type(content).__name__)
|
||||
elif content < 0:
|
||||
raise ValueError('Packed values must be positive (attempted to pack %i as a %s)' % (content, self.name))
|
||||
|
||||
try:
|
||||
packed = struct.pack(self.format, content)
|
||||
except struct.error:
|
||||
if not stem.util._is_int(content):
|
||||
raise ValueError('Size.pack encodes an integer, but was a %s' % type(content).__name__)
|
||||
elif content < 0:
|
||||
raise ValueError('Packed values must be positive (attempted to pack %i as a %s)' % (content, self.name))
|
||||
else:
|
||||
raise # some other struct exception
|
||||
|
||||
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):
|
||||
to_unpack, remainder = split(packed, self.size)
|
||||
|
||||
return self.unpack(to_unpack), remainder
|
||||
|
||||
def __hash__(self):
|
||||
return stem.util._hash_attr(self, 'name', 'size', 'format', cache = True)
|
||||
|
||||
|
||||
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("'%s' isn't an IPv4 or IPv6 address" % value)
|
||||
|
||||
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 = b''.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:i + 1])) 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 = b''.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 = bytearray()
|
||||
cell += Size.CHAR.pack(self.type_int)
|
||||
cell += Size.CHAR.pack(len(self.value_bin))
|
||||
cell += self.value_bin
|
||||
return bytes(cell)
|
||||
|
||||
@staticmethod
|
||||
def pop(content):
|
||||
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 stem.util._hash_attr(self, 'type_int', 'value_bin', cache = True)
|
||||
|
||||
|
||||
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 = bytearray()
|
||||
cell += Size.CHAR.pack(self.type_int)
|
||||
cell += Size.SHORT.pack(len(self.value))
|
||||
cell += self.value
|
||||
return bytes(cell)
|
||||
|
||||
@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 stem.util._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 = b''
|
||||
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)
|
||||
|
||||
|
||||
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'))
|
||||
Loading…
Add table
Add a link
Reference in a new issue