MAX_RPAYLOAD_LEN = 498 class Create2Cell(FixedLenCell):
    '''.. note:: tor-spec, Section 5.1'''

    def __init__(self, header, htype=None, hlen=None, hdata=None):
        '''
        :param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
            initialized header to use with this cell
        :param int htype: Handshake type in use
        :param int hlen: Length of the handshake data
        :param str hdata: Actual handshake data to use (onion skin)
        '''
        self.header = header
        self.htype = htype
        self.hlen = hlen
        self.hdata = hdata

    @staticmethod
    def make(circ_id, htype=DEF.NTOR_HTYPE, hlen=DEF.NTOR_HLEN, hdata='',
             link_version=3):
        '''Build and return a Create2 cell, using default values where
        possible.

        Automatically create and use an appropriate FixedLenCell.Header.

        .. note: oppy only supports the NTor handshake, so *make()* will
            currently reject any *htype*'s or *hlen*'s that are not
            recognized as used in the NTor handshake.

        :param int circ_id: Circuit ID to use for this cell
        :param int hlen: Length of **hdata** segment
        :param str hdata: Actual handshake data to use (an *onion skin*)
        :param int link_version: Link Protocol version in use
        :returns: :class:`~oppy.cell.fixedlen.Create2Cell`
        '''
        if htype != DEF.NTOR_HTYPE:
            msg = 'htype was {}, but we currently only can do '
            msg += '{} (NTor)'
            raise BadPayloadData(msg.format(htype, DEF.NTOR_HTYPE))

        if hlen != DEF.NTOR_HLEN:
            msg = 'htype was NTor but hlen was {}, expected {}.'
            raise BadPayloadData(msg.format(hlen, DEF.NTOR_HLEN))

        if hlen != len(hdata):
            msg = 'hlen was {}, but len(hdata) was {}.'
            raise BadPayloadData(msg.format(hlen, len(hdata)))

        h = FixedLenCell.Header(circ_id=circ_id,
                                cmd=DEF.CREATE2_CMD,
                                link_version=link_version)

        return Create2Cell(h, htype=htype, hlen=hlen, hdata=hdata)

    def getBytes(self, trimmed=False):
        '''Construct and return the byte string represented by this cell.

        :param bool trimmed: If **True**, return just the cell bytes with no
            padding.  Otherwise, pad cell bytes out to fixed-length size
            according to Link Protocol version in use.
        :returns: **str** formatted byte string represented by this cell
        '''
        ret = self.header.getBytes()
        ret += struct.pack('!H', self.htype) + struct.pack('!H', self.hlen)
        ret += self.hdata
        if trimmed is True:
            return ret
        else:
            return FixedLenCell.padCellBytes(ret, self.header.link_version)

    def _parsePayload(self, data):
        '''Parse the string *data* and extract cell fields.

        Set this cell's attributes from extracted values.

        :param str data: string to parse
        '''
        start, end = self.payloadRange()
        offset = start

        if end - start < HTYPE_LEN + HLEN_LEN:
            msg = "Create2Cell payload was not enough bytes to construct "
            msg += "a valid Create2Cell."
            raise BadPayloadData(msg)

        self.htype = struct.unpack('!H', data[offset:offset + HTYPE_LEN])[0]

        if self.htype != DEF.NTOR_HTYPE:
            msg = "Create2 got htype: {}, but oppy only supports ntor: {}."
            raise BadPayloadData(msg.format(self.htype, DEF.NTOR_HTYPE))

        offset += HTYPE_LEN

        self.hlen = struct.unpack('!H', data[offset:offset + HLEN_LEN])[0]

        if self.hlen != DEF.NTOR_HLEN:
            msg = "Create2 got hlen: {}, but oppy only supports ntor hlen: {}."
            raise BadPayloadData(msg.format(self.hlen, DEF.NTOR_HLEN))

        offset += HLEN_LEN

        try:
            self.hdata = data[offset:offset + self.hlen]
        except IndexError:
            msg = "Create2 hlen was specified to be {} bytes, but actual "
            msg += "hdata was {} bytes."
            raise BadPayloadData(msg.format(self.hlen, len(data) - offset))

    def __repr__(self):
        fmt = '{}, htype={}, hlen={}, hdata={}'
        fmt = 'Create2Cell({})'.format(fmt)
        return fmt.format(repr(self.header), repr(self.htype),
                          repr(self.hlen), repr(self.hdata)) class DestroyCell(FixedLenCell):
    '''.. note:: tor-spec, Section 5.4'''

    def __init__(self, header, reason=None):
        '''
        :param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
            Initialized header to use in this cell
        :param int reason: Reason this DestroyCell was being sent.
        '''
        self.header = header
        self.reason = reason

    # DESTROY_NONE should always be sent forward to avoid leaking version
    @staticmethod
    def make(circ_id, reason=DEF.DESTROY_NONE, link_version=3):
        '''Build and return a Destroy cell, using default values where
        possible.

        Automatically create and use an appropriate FixedLenCell.Header.

        .. warning: reason 0 (DESTROY_NONE in oppy.cell.definitions)
            should always be sent forward to avoid leaking version
            information.

        :param int circ_id: Circuit ID to use for this cell
        :param int reason: Reason this DESTROY cell is being sent
        :param int link_version: Link Protocol version in use
        :returns: :class:`~oppy.cell.fixedlen.DestroyCell`
        '''
        h = FixedLenCell.Header(circ_id=circ_id,
                                cmd=DEF.DESTROY_CMD,
                                link_version=link_version)

        if reason not in DEF.DESTROY_TRUNCATE_REASONS:
            msg = 'Unrecognized DESTROY reason: {}'.format(reason)
            raise BadPayloadData(msg)

        return DestroyCell(h, reason=reason)

    def getBytes(self, trimmed=False):
        '''Construct and return the byte string represented by this cell.

        :param bool trimmed: If **True**, return just the bytes without
            padding. Otherwise, pad length out to fixed-length cell size
            according to Link Protocol version in use.
        :returns: **str** raw byte string this cell represents
        '''
        ret = self.header.getBytes()
        ret += struct.pack('!B', self.reason)
        if trimmed is True:
            return ret
        else:
            return FixedLenCell.padCellBytes(ret, self.header.link_version)

    def _parsePayload(self, data):
        '''Parse the string *data* and extract cell fields.

        Set this cell's attributes.

        :param str data: string to parse
        '''
        start, _ = self.payloadRange()
        self.reason = struct.unpack('!B', data[start:start + REASON_LEN])[0]
        if self.reason not in DEF.DESTROY_TRUNCATE_REASONS:
            msg = 'Unrecognized DESTROY reason: {}'.format(self.reason)
            raise BadPayloadData(msg)

    def __repr__(self):
        fmt = '{}, reason={}'
        fmt = 'DestroyCell({})'.format(fmt)
        return fmt.format(repr(self.header), repr(self.reason)) class EncryptedCell(FixedLenCell):
    '''
    .. note::
        EncryptedCell is not a defined cell type in tor-spec, but
        we use it as a convenient way to represent RELAY cells or
        RELAY_EARLY cells that have either been encrypted by oppy or
        received from the network and have not been decrypted yet.
    '''

    def __init__(self, header, enc_payload=None):
        '''
        :param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
            header to use with this cell
        :param str enc_payload: encrypted payload for use with this cell
        '''
        self.header = header
        self.enc_payload = enc_payload

    @staticmethod
    def make(circ_id, payload, link_version=3, early=False):
        '''Build and return a Destroy cell, using default values where
        possible.

        Automatically create and use an appropriate FixedLenCell.Header. The
        *early* parameter specifies whether we should send a RELAY cell or
        a RELAY_EARLY cell.

        .. warning::

            RELAY_EARLY cells should always be used during circuit creation
            to avoid certain classes of attacks. That is, whenever oppy
            sends a relay EXTEND2 cell, it would be sent as a RELAY_EARLY
            cell instead of a RELAY cell.

            Reference: tor-spec, Section 5.6

        .. note: *payload* field should be fully padded and equal to
            maximum relay cell payload length (498).

        :param int circ_id: Circuit ID to use for this cell
        :param str payload: Payload bytes to use in this cell
        :param int link_version: Link Protocol version in use
        :param bool early: Dictate whether or not to use a RELAY_EARLY cell
        :returns: :class:`~oppy.cell.fixedlen.EncryptedCell`
        '''
        if len(payload) != DEF.MAX_PAYLOAD_LEN:
            msg = 'EncryptedCell enc_payload should be padding to length {}; '
            msg += 'found enc_payload length {} instead.'
            msg = msg.format(DEF.MAX_PAYLOAD_LEN, len(payload))
            raise BadPayloadData(msg)

        cmd = DEF.RELAY_EARLY_CMD if early is True else DEF.RELAY_CMD

        h = FixedLenCell.Header(circ_id=circ_id, cmd=cmd,
                                link_version=link_version)

        return EncryptedCell(h, enc_payload=payload)

    def getBytes(self, trimmed=False):
        '''Construct and return the byte string represented by this cell.

        :param bool trimmed: ignored, encrypted cell's don't know anything
            about their payload or its length
        :returns: **str** raw byte string this cell represents
        '''
        return self.header.getBytes() + self.enc_payload

    def _parsePayload(self, data):
        '''Parse the string *data* and extract cell fields.

        .. note::

            EncryptedCell does not try to interpret the payload,
            assuming that it is encrypted and unreadable and will be
            decrypted and parsed somewhere else.

        :param str data: string to parse
        '''
        start, end = self.payloadRange()
        self.enc_payload = data[start:end]

    def __repr__(self):
        fmt = "EncryptedCell({}, enc_payload={})"
        return fmt.format(repr(self.header), repr(self.enc_payload)) class RelayDataCell(RelayCell):
    '''.. note:: tor-spec, Section 6.2

    .. note: RelayDataCell's don't have any fields beyond the header
        and relay header, so the payload is just treated as a blob.
    '''

    @staticmethod
    def make(circ_id, stream_id, rpayload, link_version=3):
        '''Construct and return a RelayData cell, using default values
        where possible.

        Create a FixedLenCell.Header and a RelayCell.RelayHeader.

        :param int circ_id: Circuit ID to use in this cell
        :param int stream_id: Stream ID to use in this cell
        :param str rpayload: data to use as the relay payload
        :param int link_version: Link Protocol version in use
        '''
        if len(rpayload) > DEF.MAX_RPAYLOAD_LEN:
            raise BadPayloadData()
        h = FixedLenCell.Header(circ_id=circ_id,
                                cmd=DEF.RELAY_CMD,
                                link_version=link_version)
        r = RelayCell.RelayHeader(cmd=DEF.RELAY_DATA_CMD,
                                  recognized=DEF.RECOGNIZED,
                                  stream_id=stream_id,
                                  digest=DEF.EMPTY_DIGEST,
                                  rpayload_len=len(rpayload))
        return RelayDataCell(h, rheader=r, rpayload=rpayload) class RelayEndCell(RelayCell):
    '''.. note:: tor-spec, Section 6.3'''

    def __init__(self, header, rheader=None, reason=None, reason_data=None):
        '''
        :param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
            fixed-length header to use in this cell
        :param :class:`~oppy.cell.relay.RelayCell.RelayHeader` rheader:
            relay header to use in this cell
        :param int reason: Single byte that describes the reason this stream
            was closed
        :param str reason_data: with REASON_EXITPOLICY, this optional field
            may be filled in
        '''
        self.header = header
        self.rheader = rheader
        self.reason = reason
        self.reason_data = reason_data

    @staticmethod
    def make(circ_id, stream_id, reason=DEF.REASON_DONE, reason_data='',
             link_version=3):
        '''Construct and return a RelayEnd cell, using default values where
        possible.

        Create a FixedLenCell.Header and a RelayCell.RelayHeader.

        :param int circ_id: Circuit ID to use in this cell
        :param int stream_id: Stream ID to use in this cell
        :param int reason: Single byte that describes the reason this stream
            was closed
        :param str reason_data: with REASON_EXITPOLICY, this optional field
            may be filled in
        :returns: :class:`~oppy.cell.relay.RelayEndCell`
        '''
        h = FixedLenCell.Header(circ_id=circ_id,
                                cmd=DEF.RELAY_CMD,
                                link_version=link_version)
        r = RelayCell.RelayHeader(cmd=DEF.RELAY_END_CMD,
                                  recognized=DEF.RECOGNIZED,
                                  stream_id=stream_id,
                                  digest=DEF.EMPTY_DIGEST,
                                  rpayload_len=REASON_SIZE + len(reason_data))
        return RelayEndCell(h, rheader=r, reason=reason,
                            reason_data=reason_data)

    def getBytes(self, trimmed=False):
        '''Build and return the raw byte string this cell represents.

        :param bool trimmed: if **True**, do not pad payload bytes
        :returns: **str** raw byte string this cell represents
        '''
        ret = self.header.getBytes() + self.rheader.getBytes()
        ret += struct.pack('!B', self.reason)
        ret += self.reason_data
        if trimmed is True:
            return ret
        else:
            return FixedLenCell.padCellBytes(ret, self.header.link_version)

    def _parseRelayPayload(self, data):
        '''Parse the string *data* and extract RelayEndCell payload values.

        Fill in this cell's fields with values.

        :param str data: data string to parse
        '''
        start, end = self.relayPayloadRange()
        offset = start
        self.reason = struct.unpack('!B', data[offset:offset + REASON_SIZE])[0]
        offset += REASON_SIZE

        if self.reason == DEF.REASON_EXITPOLICY:
            self.reason_data = data[offset:end]
        else:
            self.reason_data = ''

    def __repr__(self):
        fmt = '{}, rheader={}, reason={}, reason_data={}'
        fmt = 'RelayBeginCell({})'.format(fmt)
        return fmt.format(repr(self.header), repr(self.rheader),
                          repr(self.reason), repr(self.reason_data)) class RelayConnectedCell(RelayCell):
    '''.. note:: tor-spec, Section 6.2'''

    def __init__(self, header, rheader=None, addr_type=None, addr=None,
                 ttl=None):
        '''
        :param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
            fixed-length header to use in this cell
        :param :class:`~oppy.cell.relay.RelayCell.RelayHeader` rheader:
            relay header to use in this cell
        :param str addr_type: type of IP address sent (only set if IPv6)
        :param str addr: address a connection has been made to
        :param str ttl: number of seconds the address can be cached
        '''
        self.header = header
        self.rheader = rheader
        self.addr_type = addr_type
        self.addr = addr
        self.ttl = ttl

    def getBytes(self, trimmed=False):
        '''Build and return the raw byte string this cell represents.

        :param bool trimmed: if **True**, do not pad payload bytes
        :returns: **str** raw bytes this cell represents
        '''
        ret = self.header.getBytes() + self.rheader.getBytes()
        ret += self.addr + self.addr_type + self.ttl
        if trimmed is True:
            return ret
        else:
            return FixedLenCell.padCellBytes(ret, self.header.link_version)

    def _parseRelayPayload(self, data):
        '''Parse the string *data* and extract cell fields.

        :param str data: string to parse
        '''
        start, _ = self.relayPayloadRange()
        offset = start
        self.addr = data[offset:offset + DEF.IPv4_ADDR_LEN]
        offset += DEF.IPv4_ADDR_LEN

        # ZERO_ADDR_STR indicates we have an IPv6 address
        if self.addr == ZERO_ADDR_STR:
            self.addr_type = data[offset:offset + ADDR_TYPE_LEN]
            offset += ADDR_TYPE_LEN
            self.addr = data[offset:offset + DEF.IPv6_ADDR_LEN]
            offset += DEF.IPv6_ADDR_LEN
        else:
            self.addr_type = ''

        self.ttl = data[offset:offset + TTL_LEN]

    def __repr__(self):
        fmt = '{}, rheader={}, addr_type={}, addr={}, ttl={}'
        fmt = 'RelayConnectedCell=({})'.format(fmt)
        return fmt.format(repr(self.header), repr(self.rheader),
                          repr(self.addr_type), repr(self.addr),
                          repr(self.ttl)) class RelaySendMeCell(RelayCell):
    '''.. note:: tor-spec, Section 7.3, 7.4

    .. note: There are no fields in a SendMe cell's payload, so
        we just use parent class fields and methods.
    '''

    # convenience function to simplify construction
    @staticmethod
    def make(circ_id, stream_id=0, link_version=3):
        '''Construct and return a RelaySendMeCell, using default values
        where possible.

        Create a FixedLenCell.Header and a RelayCell.RelayHeader for use
        in this cell.

        :param int circ_id: Circuit ID to use in this cell
        :param int stream_id: Stream ID to use in this cell. A Stream ID of
            zero indicates a 'circuit-level' SendMe cell, and a non-zero
            Stream ID indicates a 'stream-level' SendMe cell.
        :param int link_version: Link Protocol version in use
        :returns: :class:`~oppy.cell.relay.RelaySendMeCell`
        '''
        h = FixedLenCell.Header(circ_id=circ_id,
                                cmd=DEF.RELAY_CMD,
                                link_version=link_version)
        r = RelayCell.RelayHeader(cmd=DEF.RELAY_SENDME_CMD,
                                  recognized=DEF.RECOGNIZED,
                                  stream_id=stream_id,
                                  digest=DEF.EMPTY_DIGEST,
                                  rpayload_len=0)
        return RelaySendMeCell(h, r) class RelayExtendedCell(RelayCell):
    '''.. note:: Not Implemented'''
    def __init__(self, header):
        raise NotImplementedError("Can't make RelayExtendedCell yet.") class RelayTruncatedCell(RelayCell):
    '''.. note:: tor-spec, Section 5.4'''

    def __init__(self, header, rheader=None, reason=None):
        '''
        :param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
            header to use in this cell
        :param :class:`~oppy.cell.relay.RelayCell.RelayHeader` rheader:
            relay header to use in this cell
        :param int reason: A single byte describing the reason this
            RelayTruncatedCell was sent.
        '''
        self.header = header
        self.rheader = rheader
        self.reason = reason

    def getBytes(self, trimmed=False):
        '''Build and return the raw byte string this cell represents.

        :param bool trimmed: if **True**, do not pad payload bytes
        :returns: **str** raw bytes this cell represents
        '''
        ret = self.header.getBytes() + self.rheader.getBytes()
        ret += struct.pack('!B', self.reason)
        if trimmed is True:
            return ret
        else:
            return FixedLenCell.padCellBytes(ret, self.header.link_version)

    def _parseRelayPayload(self, data):
        '''Parse the string *data* and extract RelayTruncatedCell fields.

        Fill in this cell's attributes.

        :param str data: data string to parse
        '''
        start, _ = self.relayPayloadRange()
        self.reason = struct.unpack('!B', data[start:start + REASON_SIZE])[0]

    def __repr__(self):
        fmt = 'RelayTruncatedCell=({}, rheader={}, reason={})'
        return fmt.format(repr(self.header), repr(self.rheader),
                          repr(self.reason)) class RelayDropCell(RelayCell):
    '''.. note:: tor-spec, Section 6.2

    .. note: RelayDropCell is a long-range dummy cell and they are immediately
        dropped upon receipt.
    '''
    pass class RelayResolvedCell(RelayCell):
    '''.. note:: Not Implemented'''
    pass class RelayExtended2Cell(RelayCell):
    '''.. note:: tor-spec, Section 5.1, 5.1.2'''

    def __init__(self, header, rheader=None, hlen=None, hdata=None):
        '''
        :param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
            header to use in this cell
        :param :class:`~oppy.cell.relay.RelayCell.RelayHeader` rheader:
            relay header to use in this cell
        :param int hlen: length of the handshake data field
        :param str hdata: handshake data (onion skin)
        '''
        self.header = header
        self.rheader = rheader
        self.hlen = hlen
        self.hdata = hdata

    def getBytes(self, trimmed=False):
        '''Build and return the raw byte string this cell represents.

        :param bool trimmed: if **True**, do not pad payload bytes
        :returns: **str** raw byte string this cell represents
        '''
        ret = self.header.getBytes() + self.rheader.getBytes()
        ret += struct.pack('!H', self.hlen)
        ret += self.hdata
        if trimmed is True:
            return ret
        else:
            return FixedLenCell.padCellBytes(ret, self.header.link_version)

    def _parseRelayPayload(self, data):
        '''Parse the string *data* and extract Extended2 fields.

        Fill in this cell's attributes.

        :param str data: data string to parse
        '''
        start, _ = self.relayPayloadRange()
        offset = start

        self.hlen = struct.unpack('!H', data[offset:offset + HLEN_LEN])[0]
        offset += HLEN_LEN

        try:
            self.hdata = data[offset:offset + self.hlen]
        except IndexError:
            raise BadPayloadData('Not enough hdata bytes.')

    def __repr__(self):
        fmt = 'RelayExtended2Cell({}, rheader={}, hlen={}, hdata={})'
        return fmt.format(repr(self.header), repr(self.rheader),
                          repr(self.hlen), repr(self.hdata)) class RelayExtendCell(RelayCell):
    '''.. note:: Not Implemented'''
    pass class Circuit(object):

    def __init__(self, circuit_manager, circuit_id, conn, circuit_type,
                 path, crypt_path, max_streams=MAX_STREAMS_V3):
        '''
        :param int cid: id of this circuit
        '''
        self._circuit_manager = circuit_manager
        self.circuit_id = circuit_id
        self._connection = conn
        self.circuit_type = circuit_type
        self._path = path
        self._crypt_path = crypt_path
        # _read_queue handles incoming cells from the network
        self._read_queue = defer.DeferredQueue()
        self._read_task = None
        # _write_queue handles incoming data from local applications
        self._write_queue = defer.DeferredQueue()
        self._write_task = None
        self._streams = {}
        self._ctr = ctr(max_streams)
        self._max_streams = max_streams
        self._state = CState.OPEN
        # deliver window is incoming data cells
        self._deliver_window = CIRCUIT_WINDOW_THRESHOLD_INIT
        # package window is outgoing data cells
        self._package_window = CIRCUIT_WINDOW_THRESHOLD_INIT

        self._pollReadQueue()
        self._pollWriteQueue()

    def canHandleRequest(self, request):
        '''Return **True** if this circuit can (probably/possibly) handle
        the *request*.

        If this circuit is pending we may not have a relay exit relay whose
        exit policy we can check, so make a guess and return True if the
        request is of the same type as this circuit. Always return True if
        this request is a host type request (this is probably wrong). If the
        circuit is open and we do have an exit policy to check, then return
        whether or not this circuit's exit relay's exit policy claims to
        support this request.

        :param oppy.util.exitrequest.ExitRequest request: the request to
            check if this circuit can handle
        :returns: **bool** **True** if this circuit thinks it can handle
            the request, False otherwise
        '''
        if self._state == CState.BUFFERING:
            return False

        if len(self._streams) == self._max_streams:
            return False

        return self._path.exit.microdescriptor.exit_policy.can_exit_to(port=request.port)

    def send(self, data, stream):
        '''Put a tuple of (data, stream_id) on this circuit's write_queue.

        Called by stream's when they want to write data to this circuit.

        .. warning:: writeData() requires that *data* can fit in a single
            relay cell. The caller should take care to split data into
            properly sized chunks.

        :param str data: data string to write to this circuit
        :param int stream_id: id of the stream writing this data
        '''
        if len(data) > MAX_RPAYLOAD_LEN:
            msg = ("Data cannot be longer than {}. len(data) == {}."
                   .format(MAX_RPAYLOAD_LEN, len(data)))
            raise ValueError(msg)
        self._write_queue.put((data, stream.stream_id))

    def recv(self, cell):
        '''Put the incoming cell on this circuit's read_queue to be processed.

        Called be a connection when it receives a cell addressed to this
        circuit.

        :param cell cell: incoming cell that was received from the network
        '''
        self._read_queue.put(cell)

    def removeStream(self, stream):
        '''Unregister *stream* from this circuit.

        Remove the stream from this circuit's stream map and send a
        RelayEndCell. If the number of streams on this circuit drops to
        zero, check with the circuit manager to see if this circuit should
        be destroyed. If so, tear down the circuit.

        :param oppy.stream.stream.Stream stream: stream to unregister
        '''
        try:
            del self._streams[stream.stream_id]
            cell = RelayEndCell.make(self.circuit_id, stream.stream_id)
            self._encryptAndSendCell(cell)
        except KeyError:
            msg = ("Circuit {} notified that stream {} was closed, but "
                   "the circuit has no reference to this stream."
                   .format(self.circuit_id, stream.stream_id))
            logging.debug(msg)
            return

        if len(self._streams) == 0:
            if self._circuit_manager.shouldDestroyCircuit(self) is True:
                self._sendDestroyCell()
                self._closeCircuit()
                msg = "Destroyed unused circuit {}.".format(self.circuit_id)
                logging.debug(msg)

    def beginStream(self, stream):
        '''Initiate a new stream by sending a RelayBeginCell.

        Create the begin cell, encrypt it, and immediately write it to this
        circuit's connection.

        :param oppy.stream.stream.Stream stream: stream on behalf of which
            we're sending a RelayBeginCell
        '''
        msg = ("Circuit {} sending a RelayBeginCell for stream {}."
               .format(self.circuit_id, stream.stream_id))
        logging.debug(msg)

        cell = RelayBeginCell.make(self.circuit_id, stream.stream_id,
                                   stream.request)
        self._encryptAndSendCell(cell)

    def addStreamAndSetStreamID(self, stream):
        '''Register the new *stream* on this circuit.

        Set the stream's stream_id and add it to this circuit's stream map.

        :param oppy.stream.stream.Stream stream: stream to add to this circuit
        '''
        # find the next available stream ID and assign it to the requesting
        # stream. fail if we can't assign an ID (should never happen).
        if len(self._streams) == self._max_streams:
            msg = ("Circuit {} tried to add a stream, but it's stream map was "
                   "full. This is a bug.".format(self.circuit_id))
            raise RuntimeError(msg)

        assigned = False
        for _ in xrange(self._max_streams):
            _id = next(self._ctr)
            if _id not in self._streams:
                self._streams[_id] = stream
                stream.stream_id = _id
                assigned = True
                break

        if not assigned:
            msg = ("Circuit {} failed to assign a requesting stream an ID, "
                   "even though it's stream map only contains {} streams. "
                   "This is a bug."
                   .format(self.circuit_id, len(self._streams)))
            raise RuntimeError(msg)

        msg = ("Circuit {} added a new stream {}."
               .format(self.circuit_id, stream.stream_id))
        logging.debug(msg)

    def sendStreamSendMe(self, stream):
        '''Send a stream-level RelaySendMe cell with its stream_id equal to
        *stream_id*.

        Construct the send me cell, encrypt it, and immediately write it to
        this circuit's connection.

        :param int stream_id: stream_id to use in the RelaySendMeCell
        '''
        cell = RelaySendMeCell.make(self.circuit_id,
                                    stream_id=stream.stream_id)
        self._encryptAndSendCell(cell)

        msg = ("Circuit {} sent a RelaySendMeCell for stream {}."
               .format(self.circuit_id, stream.stream_id))
        logging.debug(msg)

    def destroyCircuitFromManager(self):
        '''Called by the circuit manager when it decides to destroy this
        circuit.

        Send a destroy cell and notify this circuit's connection that this
        circuit is now closed.
        '''
        msg = ("Circuit {} destroyed by circuit manager."
               .format(self.circuit_id))
        logging.debug(msg)
        self._sendDestroyCell()
        self._closeAllStreams()
        self._connection.removeCircuit(self)

    def destroyCircuitFromConnection(self):
        '''Called when a connection closes this circuit (usually because
        the connection went down).

        Primarily called when we lose the TLS connection to our connection
        object.  Do a 'hard' destroy and immediately close all associated
        streams.  Do not send a destroy cell.
        '''
        msg = ("Circuit {} destroyed by its connection."
               .format(self.circuit_id))
        logging.debug(msg)
        self._closeCircuit()

    def _pollReadQueue(self):
        '''Try pulling a cell from this circuit's read_queue and add a
        callback to handle the cell when it's available.
        '''
        self._read_task = self._read_queue.get()
        self._read_task.addCallback(self._recvCell)

    def _pollWriteQueue(self):
        '''Try pulling data from this circuit's write_queue and add a
        callback to process the data when it's available.
        '''
        self._write_task = self._write_queue.get()
        self._write_task.addCallback(self._writeData)

    def _writeData(self, data_stream_id_tuple):
        '''Write data to this circuit's connection.

        Do the following:

            1. Package this data (with appropriate stream_id) into a
               RelayDataCell.
            2. Encrypt this cell.
            3. Write this cell to this circuit's connection.
            4. Decrement this circuit's packaging window (if we can't
               package anymore data, enter state CState.BUFFERING, otherwise
               begin polling from _write_queue again).

        :param tuple, str, int data_stream_id_tuple: tuple of (data, stream_id)
            to package into a RelayData cell
        '''
        data, stream_id = data_stream_id_tuple
        cell = RelayDataCell.make(self.circuit_id, stream_id, data)
        self._encryptAndSendCell(cell)
        self._decPackageWindow()

    def _recvCell(self, cell):
        '''Called when this circuit receives a cell and it's state is
        CState.OPEN.

        If we received a non-backward cell or a DestroyCell, immediately
        tear-down the circuit. Otherwise process as usual.

        :param cell cell: the cell received from the network.
        '''
        if type(cell) not in BACKWARD_CELL_TYPES:
            msg = ("Circuit {} received a {} cell that violates the Tor "
                   "protocol. Destroying circuit."
                   .format(self.circuit_id, type(cell)))
            logging.warning(msg)
            self._sendDestroyCell()
            self._closeCircuit()
        elif isinstance(cell, DestroyCell):
            msg = ("Circuit {} received a DestroyCell. Tearing down circuit."
                   .format(self.circuit_id))
            logging.debug(msg)
            self._closeCircuit()
        else:
            self._recvRelayCell(cell)
            self._pollReadQueue()

    def _recvRelayCell(self, cell):
        '''Called when this circuit receives some sort of RelayCell from
        the network.

        Decrypt this cell and take action based on the cell type and this
        circuit's current state. Each valid backward cell type has a
        handler function that's called when that cell type is received.

        .. note:: oppy just drops any unrecognized cells.

        :param cell cell: cell received from the network
        '''
        try:
            cell, origin = crypto.decryptCell(cell, self._crypt_path)
        except Exception as e:
            msg = ("Circuit {} failed to decrypt an incoming cell. Reason: {}"
                   ". Dropping cell.".format(self.circuit_id, e))
            logging.debug(msg)
            return

        if type(cell) not in BACKWARD_RELAY_CELL_TYPES:
            msg = ("Circuit {} received a non-backward {} relay cell. This "
                   "is a violation of the Tor protocol, and the circuit will "
                   "be destroyed.".format(self.circuit_id, type(cell)))
            logging.warning(msg)
            self._sendDestroyCell()
            self._closeCircuit()
        elif isinstance(cell, RelayDataCell):
            self._processRelayDataCell(cell, origin)
        elif isinstance(cell, RelayEndCell):
            self._processRelayEndCell(cell, origin)
        elif isinstance(cell, RelayConnectedCell):
            self._processRelayConnectedCell(cell, origin)
        elif isinstance(cell, RelaySendMeCell):
            self._processRelaySendMeCell(cell, origin)
        elif isinstance(cell, RelayTruncatedCell):
            self._processRelayTruncatedCell(cell, origin)
        elif isinstance(cell, RelayDropCell):
            self._processRelayDropCell(cell, origin)
        elif isinstance(cell, RelayResolvedCell):
            self._processRelayResolvedCell(cell, origin)
        else:
            msg = ("Circuit {} received an unexpected backward cell {} "
                   "from relay in position {}. Dropping cell."
                   .format(self.circuit_id, type(cell), origin))
            logging.debug(msg)

    def _processRelayDataCell(self, cell, origin):
        '''Called when this circuit receives an incoming RelayData cell.

        Take the following actions:

            1. Pass the relay payload in this cell to the stream with the
               stream_id contained in this RelayData cell. Drop the cell
               if we have no reference to the stream_id contained in the
               cell.
            2. Decrement this circuit's delivery window (which will
               automatically send a RelaySendMeCell if this circuit's
               deliver window is low enough).

        :param oppy.cell.relay.RelayDataCell cell: relay data cell recieved
            from the network
        :param int origin: which node on the circuit's path this cell
            came from
        '''
        sid = cell.rheader.stream_id

        try:
            self._streams[sid].recv(cell.rpayload)
            self._decDeliverWindow()
        except KeyError:
            msg = ("Circuit {} received a RelayDataCell for nonexistent "
                   "stream {}. Dropping cell.".format(self.circuit_id, sid))
            logging.debug(msg)

    def _processRelayEndCell(self, cell, origin):
        '''Called when this circuit receives a RelayEndCell.

        Tear down the stream associated with the stream in the RelayEndCell
        if this circuit has a reference to it. Drop the cell if we have
        no reference to this stream.

        :param oppy.cell.relay.RelayEndCell cell: relay end cell recieved
            from the network
        :param int origin: which node on the circuit's path this cell
            came from
        '''
        sid = cell.rheader.stream_id

        try:
            self._streams[sid].closeFromCircuit()
            # TODO: handle REASON_EXITPOLICY
            if cell.reason != REASON_DONE:
                msg = ("Circuit {} received a RelayEndCell for stream {}, "
                       "and reason was not REASON_DONE. Reason: {}."
                       .format(self.circuit_id, sid, cell.reason))
                logging.debug(msg)
        except KeyError:
            msg = ("Circuit {} received a RelayEndCell for nonexistent "
                   "stream {}. Dropping cell.".format(self.circuit_id, sid))
            logging.debug(msg)

    def _processRelayConnectedCell(self, cell, origin):
        '''Called when this circuit receives a RelayConnectedCell.

        Notify the stream associated with this cell's stream id that it's
        now connected. Drop the cell if we have no reference to this
        stream id.

        :param oppy.cell.relay.RelayConnectedCell cell: relay connected cell
            recieved from the network
        :param int origin: which node on the circuit's path this cell
            came from
        '''
        sid = cell.rheader.stream_id

        try:
            self._streams[sid].streamConnected()
        except KeyError:
            msg = ("Circuit {} received a RelayConnectedCell for nonexistent "
                   "stream {}. Dropping cell.".format(self.circuit_id, sid))
            logging.debug(msg)
            return

        msg = ("Circuit {} received a RelayConnectedCell for stream {}"
               .format(self.circuit_id, sid))
        logging.debug(msg)

    def _processRelaySendMeCell(self, cell, origin):
        '''Called when this circuit receives a RelaySendMeCell.

        If this is a circuit-level sendme cell (i.e. its stream id is zero)
        then increment this circuit's packaging window. If this circuit
        is currently in state CState.BUFFERING **and** receiving this
        sendme cell has incremented its packaging window > 0, then begin
        listening for incoming data again (i.e. self._pollWriteQueue).

        If this is a stream-level sendme cell, increment the corresponding
        stream's packaging window. Drop the cell if we have no reference
        to the stream associated with its stream id.

        Drop this cell if it's received while we're still building the
        circuit.

        :param oppy.cell.relay.RelaySendMeCell cell: relay sendme cell
            recieved from the network
        :param int origin: which node on the circuit's path this cell
            came from
        '''
        sid = cell.rheader.stream_id

        if sid == 0:
            self._incPackageWindow()
        else:
            try:
                self._streams[sid].incPackageWindow()
            except KeyError:
                msg = ("Circuit {} received a RelaySendMe cell on nonexistent"
                       " stream {}. Dropping cell."
                       .format(self.circuit_id, sid))
                logging.debug(msg)

    def _processRelayTruncatedCell(self, cell, origin):
        '''Called when this circuit receives a RelayTruncatedCell.

        oppy currently doesn't know how to rebuild or cannabalize circuits,
        so we just destroy the whole circuit if we get a truncated cell.

        :param oppy.cell.relay.RelayTruncatedCell cell: relay truncated cell
            recieved from the network
        :param int origin: which node on the circuit's path this cell
            came from
        '''
        msg = ("Circuit {} received a RelayTruncatedCell. oppy can't "
               "rebuild or cannabalize circuits yet, so the circuit will "
               "be destroyed.".format(self.circuit_id))
        logging.debug(msg)
        self._sendDestroyCell()
        self._closeCircuit()

    def _processRelayDropCell(self, cell, origin):
        '''Called when this circuit receives a RelayDrop cell.

        Just drop it :)

        :param oppy.cell.relay.RelayDropCell cell: relay drop cell
            recieved from the network
        :param int origin: which node on the circuit's path this cell
            came from
        '''
        msg = "Circuit {} received a RelayDropCell.".format(self.circuit_id)
        logging.debug(msg)

    def _processRelayResolvedCell(self, cell, origin):
        '''Called when this circuit receives a RelayResolvedCell.

        oppy doesn't know how to handle these right now, so we just drop
        them.

        :param oppy.cell.relay.RelayResolvedCell cell: relay resolved cell
            recieved from the network
        :param int origin: which node on the circuit's path this cell
            came from
        '''
        msg = ("Circuit {} received a RelayResolvedCell for stream {}."
               .format(self.circuit_id, cell.rheader.stream_id))
        logging.debug(msg)

    def _decDeliverWindow(self):
        '''Decrement this circuit's deliver window.

        Called when we deliver an incoming RelayDataCell's payload to
        a stream. If the delivery window is below the default threshold, send
        a RelaySendMeCell.
        '''
        self._deliver_window -= 1
        if self._deliver_window <= SENDME_THRESHOLD:
            cell = RelaySendMeCell.make(self.circuit_id)
            self._encryptAndSendCell(cell)
            self._deliver_window += WINDOW_SIZE

            msg = ("Circuit {}'s delivery window dropped to {}. The circuit "
                   "sent a circuit-level RelaySendMeCell and incremented "
                   "its delivery window to {}."
                   .format(self.circuit_id, self._deliver_window - WINDOW_SIZE,
                           self._deliver_window))
            logging.debug(msg)


    def _decPackageWindow(self):
        '''Decrement this circuit's package window.

        If the package window is above zero, listen for more incoming local
        data. Otherwise, enter a state CState.BUFFERING. In this buffering
        state, this circuit will not accept any new streams and will not
        write any data to its connection. It will leave it's buffering state
        and become open again when it receives enough RelaySendMeCell's to
        move its package window above zero again.
        '''
        self._package_window -= 1
        if self._package_window > 0:
            self._pollWriteQueue()
        else:
            self._state = CState.BUFFERING
            self._write_task = None
            msg = ("Circuit {}'s packaging window dropped to 0. The circuit "
                   "entered a buffering state.".format(self.circuit_id))
            logging.debug(msg)

    def _incPackageWindow(self):
        '''Increment this circuit's package window.

        Called when this circuit receives a RelaySendMeCell. If this circuit
        is currently in state CState.BUFFERING **and** receiving this
        sendme cell has moved this circuit's package window above zero,
        transition back to CState.OPEN and begin listening for incoming local
        data again.
        '''
        self._package_window += WINDOW_SIZE

        msg = ("Circuit {} received a circuit-level RelaySendMeCell. Its "
               "packaging window is now {}."
               .format(self.circuit_id, self._package_window))
        logging.debug(msg)

        if self._state == CState.BUFFERING and self._package_window > 0:
            self._state = CState.OPEN

            msg = ("Circuit {} has transitioned from buffering to open."
                   .format(self.circuit_id))
            logging.debug(msg)

            if self._write_task is None:
                self._pollWriteQueue()

    def _sendDestroyCell(self):
        '''Send a destroy cell.

        .. note:: reason NONE is always used when sending forward destroy
            cells to avoid leaking version information.
        '''
        cell = DestroyCell.make(self.circuit_id)
        self._connection.send(cell)

    def _closeAllStreams(self):
        for stream in self._streams.values():
            stream.closeFromCircuit()

    def _encryptAndSendCell(self, cell):
        try:
            enc = crypto.encryptCell(cell, self._crypt_path)
        # TODO: an exception here probably means the circuit needs to be
        #       torn down immediately
        except Exception as e:
            msg = ("Error: {}. Failed to encrypt a {} cell on circuit {}. "
                   "Refusing to send unencrypted cell. Dropping the cell."
                   .format(e, type(cell), self.circuit_id))
            logging.warning(msg)
            return

        try:
            self._connection.send(enc)
        # TODO: exception here probably means the circuit needs to be torn down
        #       immediately
        except Exception as e:
            msg = ("Error: {}. Failed to send an encrypted cell on circuit "
                   "{}.".format(e, self.circuit_id))
            logging.debug(msg)

    def _closeCircuit(self):
        '''Close this circuit.

        Close all associated streams, notify the circuit manager this
        circuit has closed, and notify this circuit's connection that this
        circuit has closed.
        '''
        self._closeAllStreams()
        self._circuit_manager.circuitDestroyed(self)
        self._connection.removeCircuit(self) CIRCUIT_WINDOW_THRESHOLD_INIT = 1000
SENDME_THRESHOLD = 900
WINDOW_SIZE = 100
BACKWARD_CELL_TYPES = (
    DestroyCell,
    EncryptedCell,
)
BACKWARD_RELAY_CELL_TYPES = (
    RelayDataCell,
    RelayEndCell,
    RelayConnectedCell,
    RelaySendMeCell,
    RelayExtendedCell,
    RelayExtended2Cell,
    RelayTruncatedCell,
    RelayDropCell,
    RelayResolvedCell,
    RelayExtended2Cell,
)
MAX_STREAMS_V3 = 65535 class ExitRequest(object):
    '''Represent a connection request.'''

    __slots__ = ('port', 'addr', 'host', 'is_ipv4', 'is_ipv6', 'is_host')

    def __init__(self, port, addr=None, host=None):
        '''
        :param str port: port to connect to
        :param str addr: IP address to connect to
        :param str host: hostname to connect to
        '''
        # either address or host must be set, but not both
        assert bool(addr) ^ bool(host)

        self.port = struct.unpack("!H", port)[0]
        self.addr = addr
        self.host = host
        self.is_ipv4 = False
        self.is_ipv6 = False
        self.is_host = False

        if addr:
            addr = ipaddress.ip_address(addr)
            if isinstance(addr, ipaddress.IPv4Address):
                self.is_ipv4 = True
            else:
                self.is_ipv6 = True
            self.addr = bytes(addr.exploded)
        else:
            self.is_host = True

    def __str__(self):
        # this is the format that a request should appear in in a RelayBegin
        # cell. overriding __str__ here allows us to just stick this
        # directly in a RelayBegin cell with str(request)
        if self.is_ipv4:
            ret = "{}:{}".format(self.addr, self.port)
        elif self.is_ipv6:
            ret = "[{}]:{}".format(self.addr, self.port)
        else:
            ret = "{}:{}".format(self.host, self.port)
        ret += "\x00"
        return ret def ctr(upper):
    """Return a generator for a rollover counter.

    :param int upper: Upper bound of counter.
    """
    return (i for _ in itertools.count() for i in range(1, upper))
import mock
from twisted.internet import defer
from twisted.trial import unittest
from oppy.cell.definitions import MAX_RPAYLOAD_LEN
from oppy.cell.fixedlen import Create2Cell, DestroyCell, EncryptedCell
from oppy.cell.relay import (
    RelayDataCell,
    RelayEndCell,
    RelayConnectedCell,
    RelaySendMeCell,
    RelayExtendedCell,
    RelayTruncatedCell,
    RelayDropCell,
    RelayResolvedCell,
    RelayExtended2Cell,
    RelayExtendCell,
)
from oppy.circuit.circuit import Circuit
from oppy.circuit.definitions import (
    CState,
    CircuitType,
    SENDME_THRESHOLD,
    WINDOW_SIZE
)
from oppy.util.exitrequest import ExitRequest
from oppy.util.tools import ctr
        self.assertEqual(self.circuit._pollReadQueue.call_count, 1)

    @mock.patch('oppy.circuit.circuit.logging', autospec=True)
    def test_recvCell_non_backward_cell_fail(self, mock_logging):
        self.circuit._recvRelayCell = mock.Mock()
        self.circuit._pollReadQueue = mock.Mock()
        self.circuit._sendDestroyCell = mock.Mock()
        self.circuit._closeCircuit = mock.Mock()
        cell = Create2Cell.make(ID, hdata=84*'a')

        self.circuit._recvCell(cell)

        self.assertEqual(self.circuit._sendDestroyCell.call_count, 1)
        self.assertEqual(self.circuit._closeCircuit.call_count, 1)
        self.assertEqual(self.circuit._recvRelayCell.call_count, 0)
        self.assertEqual(self.circuit._pollReadQueue.call_count, 0)
        self.assertTrue(mock_logging.warning.called)

    def test_recvCell_destroy_cell(self):
        self.circuit._recvRelayCell = mock.Mock()
        self.circuit._pollReadQueue = mock.Mock()
        self.circuit._sendDestroyCell = mock.Mock()
        self.circuit._closeCircuit = mock.Mock()
        cell = DestroyCell.make(ID)

        self.circuit._recvCell(cell)

        self.assertEqual(self.circuit._sendDestroyCell.call_count, 0)
        self.assertEqual(self.circuit._closeCircuit.call_count, 1)
        self.assertEqual(self.circuit._recvRelayCell.call_count, 0)
        self.assertEqual(self.circuit._pollReadQueue.call_count, 0)

    @mock.patch('oppy.crypto.util.decryptCell')
    @mock.patch('oppy.circuit.circuit.logging', autospec=True)
    def test_recvRelayCell_nonbackward_cell(self, mock_logging, mock_decrypt):
        cell = RelayExtendCell('test')
        mock_decrypt.return_value = (cell, 2)
        self.circuit._sendDestroyCell = mock.Mock()
        self.circuit._closeCircuit = mock.Mock()

        self.circuit._recvRelayCell(cell)

        self.assertEqual(self.circuit._sendDestroyCell.call_count, 1)
        self.assertEqual(self.circuit._closeCircuit.call_count, 1)
        self.assertTrue(mock_logging.warning.called)

    @mock.patch('oppy.crypto.util.decryptCell')
    @mock.patch('oppy.circuit.circuit.logging', autospec=True)
    def test_recvCell_relay_cell_decrypt_fail(self, mock_logging,
                                              mock_decrypt):
        mock_decrypt.side_effect = Exception
        self.circuit._sendDestroyCell = mock.Mock()
        self.circuit._closeCircuit = mock.Mock()
        self.circuit._processRelayDataCell = mock.Mock()
        self.circuit._processRelayEndCell = mock.Mock()
        self.circuit._processRelayResolvedCell = mock.Mock()
        self.circuit._processRelayTruncatedCell = mock.Mock()
        self.circuit._processRelayConnectedCell = mock.Mock()
        self.circuit._processRelaySendMeCell = mock.Mock()
        self.circuit._processRelayDropCell = mock.Mock()
        cell = EncryptedCell.make(ID, 'a'*509)

        self.circuit._recvRelayCell(cell)

        self.assertTrue(mock_logging.debug.call_count, 1)
        self.assertEqual(self.circuit._sendDestroyCell.call_count, 0)
        self.assertEqual(self.circuit._closeCircuit.call_count, 0)
        self.assertEqual(self.circuit._processRelayDataCell.call_count, 0)
        self.assertEqual(self.circuit._processRelayEndCell.call_count, 0)
        self.assertEqual(self.circuit._processRelayConnectedCell.call_count,
                         0)
        self.assertEqual(self.circuit._processRelayResolvedCell.call_count, 0)
        self.assertEqual(self.circuit._processRelayTruncatedCell.call_count,
                         0)
        self.assertEqual(self.circuit._processRelaySendMeCell.call_count, 0)
        self.assertEqual(self.circuit._processRelayDropCell.call_count, 0)

    @mock.patch('oppy.crypto.util.decryptCell')
    @mock.patch('oppy.circuit.circuit.logging', autospec=True)
    def test_recvCell_relay_unexpected_cell_fail(self, mock_logging,
                                                 mock_decrypt):
        mock_decrypt.return_value = (RelayExtended2Cell('test'), 2)
        self.circuit._sendDestroyCell = mock.Mock()
        self.circuit._closeCircuit = mock.Mock()
        self.circuit._processRelayDataCell = mock.Mock()
        self.circuit._processRelayEndCell = mock.Mock()
        self.circuit._processRelayResolvedCell = mock.Mock()
        self.circuit._processRelayTruncatedCell = mock.Mock()
        self.circuit._processRelayConnectedCell = mock.Mock()
        self.circuit._processRelaySendMeCell = mock.Mock()
        self.circuit._processRelayDropCell = mock.Mock()
        cell = EncryptedCell.make(ID, 'a'*509)

        self.circuit._recvRelayCell(cell)

        self.assertTrue(mock_logging.debug.call_count, 1)
        self.assertEqual(self.circuit._sendDestroyCell.call_count, 0)
        self.assertEqual(self.circuit._closeCircuit.call_count, 0)
        self.assertEqual(self.circuit._processRelayDataCell.call_count, 0)
        self.assertEqual(self.circuit._processRelayEndCell.call_count, 0)
        self.assertEqual(self.circuit._processRelayConnectedCell.call_count,
                         0)
        self.assertEqual(self.circuit._processRelayResolvedCell.call_count, 0)
        self.assertEqual(self.circuit._processRelayTruncatedCell.call_count,
                         0)
        self.assertEqual(self.circuit._processRelaySendMeCell.call_count, 0)
        self.assertEqual(self.circuit._processRelayDropCell.call_count, 0)
        
    @mock.patch('oppy.crypto.util.decryptCell')
    def test_recvRelayCell_data_cell(self, mock_decrypt):
        cell = RelayDataCell.make(ID, ID, 'a')
        mock_decrypt.return_value = (cell, 2)
        self.circuit._processRelayDataCell = mock.Mock()

        self.circuit._recvRelayCell(cell)

        self.circuit._processRelayDataCell.assert_called_once_with(cell, 2)

    @mock.patch('oppy.crypto.util.decryptCell')
    def test_recvRelayCell_end_cell(self, mock_decrypt):

        cell = RelayEndCell(ID, ID)