#!/usr/bin/env python3

import os
import random # For simulation, not cryptography!
import math
import sys
import logging
import resource

import nacl.utils
import nacl.signing
import nacl.public
import nacl.hash
import nacl.bindings

import network
import dirauth

import msg as simmsg
import cell as simcell

from connection import Connection

vrftable = dict()

# Simulated VRF.  The output and proof are of the correct size (32 bytes
# for the output, 48 bytes for the proof), and we charge the right
# number of group operations (1 keygen, 2 DH for proving, 3 DH for
# verifiying -- that last number charges 1 DH for a (s*G+c*Y) multiexp,
# but 2 for a (s*A+c*B) multiexp)
class VRF:
    @staticmethod
    def get_output(vrf_privkey, vrf_input, perfstats):
        """Given the VRF private key and the input, produce
        the output value and the proof."""
        # nacl can't do the group operations we need to actually
        # compute the VRF, so we just fake it for the simulation.  The
        # output value is sha256(privkey, input), and the proof is
        # sha256(pubkey, input, output) + 16 bytes of 0.
        # ***THIS IS NOT A REAL VRF!***
        val = nacl.hash.sha256(bytes(vrf_privkey) + vrf_input,
                encoder=nacl.encoding.RawEncoder)
        vrf_pubkey = vrf_privkey.public_key
        proof = nacl.hash.sha256(bytes(vrf_pubkey) + val + vrf_input,
                encoder=nacl.encoding.RawEncoder) + bytes(16)
        perfstats.keygens += 1
        perfstats.dhs += 2
        vrftable[proof] = (bytes(vrf_privkey), vrf_input, val, bytes(vrf_pubkey))
        return val, proof

    @staticmethod
    def check_output(vrf_pubkey, vrf_input, vrf_output, perfstats):
        """Given the VRF public key, the input, and the claimed output
        and proof, raise an exception if the proof fails to check.
        Returns the VRF output."""
        # Again, NOT A REAL VRF!
        val, proof = vrf_output
        if nacl.hash.sha256(vrf_pubkey + val + vrf_input,
                encoder=nacl.encoding.RawEncoder) + bytes(16) != \
                proof:
            t = [nacl.encoding.HexEncoder.encode(x) for x in vrftable[proof]]
            raise ValueError("VRF proof did not verify: %s %s %s 00x16 ?= %s: %s" % \
                    (nacl.encoding.HexEncoder.encode(vrf_pubkey),
                     nacl.encoding.HexEncoder.encode(val),
                     nacl.encoding.HexEncoder.encode(vrf_input),
                     nacl.encoding.HexEncoder.encode(proof),
                     t))
        perfstats.dhs += 3
        del vrftable[proof]
        return val


class RelayFallbackTerminationError(Exception):
    """An exception raised when someone tries to terminate a fallback
    relay."""


class Sphinx:
    """Implement the public-key reblinding technique based on Sphinx.
    This does a few more public-key operations than it would strictly
    need to if we were using a group implementation that (unlike nacl)
    supported the operations we needed directly.  The biggest issue is
    that nacl insists the high bit is set on private keys, which means
    we can't just multiply private keys together to get a new private
    key, and do a single DH operation with that resulting key; we have
    to perform a linear number of DH operations instead, per node in the
    circuit, so a quadratic number of DH operations total."""

    @staticmethod
    def makeblindkey(shared_secret, domain_separator, perfstats):
        """Create a Sphinx reblinding key (a PrivateKey) out of a shared
        secret and a domain separator (both bytestrings).  The domain
        separator is just a constant bytestring like b'data' or
        b'circuit' for the data-protecting and circuit-protecting
        public-key elements respectively."""
        rawkey = nacl.hash.sha256(domain_separator + shared_secret,
                encoder=nacl.encoding.RawEncoder)
        perfstats.keygens += 1

        # The PrivateKey constructor does the Curve25519 pinning of
        # certain bits of the key to 0 and 1
        return nacl.public.PrivateKey(rawkey)

    @staticmethod
    def reblindpubkey(blindkey, pubkey, perfstats):
        """Create a Sphinx reblinded PublicKey out of a reblinding key
        (output by makeblindkey) and a (possibly already reblinded)
        PublicKey."""
        new_pubkey =  nacl.bindings.crypto_scalarmult(bytes(blindkey),
                bytes(pubkey))
        perfstats.dhs += 1

        return nacl.public.PublicKey(new_pubkey)

    @staticmethod
    def client(client_privkey, blindkey_list, server_pubkey,
            domain_separator, is_last, perfstats):
        """Given the client's PrivateKey, a (possibly empty) list of
        reblinding keys, and the server's PublicKey, produce the shared
        secret and the new blinding key (to add to the list).  The
        domain separator is as above.  If is_last is true, don't bother
        creating the new blinding key, since this is the last iteration,
        and we won't be using it."""
        if type(server_pubkey) is bytes:
            server_pubkey = nacl.public.PublicKey(server_pubkey)
        reblinded_server_pubkey = server_pubkey
        for blindkey in blindkey_list:
            reblinded_server_pubkey = Sphinx.reblindpubkey(blindkey,
                    reblinded_server_pubkey, perfstats)

        sharedsecret = nacl.public.Box(client_privkey,
                reblinded_server_pubkey).shared_key()
        perfstats.dhs += 1

        if is_last:
            blindkey = None
        else:
            blindkey = Sphinx.makeblindkey(sharedsecret,
                    domain_separator, perfstats)

        return sharedsecret, blindkey

    @staticmethod
    def server(client_pubkey, server_privkey, domain_separator, is_last,
            perfstats):
        """Given the client's PublicKey and the server's PrivateKey,
        produce the shared secret and the new reblinded client
        PublicKey.  The domain separator is as above.  If is_last is
        True, don't bother generating the new PublicKey, since we're the
        last server in the chain, and won't be using it."""
        sharedsecret = nacl.public.Box(server_privkey,
                client_pubkey).shared_key()
        perfstats.dhs += 1

        if is_last:
            blinded_pubkey = None
        else:
            blindkey = Sphinx.makeblindkey(sharedsecret, domain_separator,
                    perfstats)

            blinded_pubkey = Sphinx.reblindpubkey(blindkey, client_pubkey,
                    perfstats)

        return sharedsecret, blinded_pubkey


class NTor:
    """A class implementing the ntor one-way authenticated key agreement
    scheme.  The details are not exactly the same as either the ntor
    paper or Tor's implementation, but it will agree on keys and have
    the same number of public key operations."""

    def __init__(self, perfstats):
        self.perfstats = perfstats
        # Only used for Single-Pass Walking Onions; it is the sequence
        # of blinding keys used by Sphinx
        self.blinding_keys = []

    def request(self):
        """Create the ntor request message: X = g^x."""
        self.client_ephem_key = nacl.public.PrivateKey.generate()
        self.perfstats.keygens += 1
        return bytes(self.client_ephem_key.public_key)

    @staticmethod
    def reply(onion_privkey, idpubkey, client_pubkey, perfstats,
            sphinx_domainsep=None):
        """The server calls this static method to produce the ntor reply
        message: (Y = g^y, B = g^b, A = H(M, "verify")) and the shared
        secret S = H(M, "secret") for M = (X^y,X^b,ID,B,X,Y). If
        sphinx_domainsep is not None, also compute and return the Sphinx
        reblinded client request to pass to the next server."""
        if type(idpubkey) is not bytes:
            idpubkey = bytes(idpubkey)
        if type(client_pubkey) is bytes:
            client_pubkey = nacl.public.PublicKey(client_pubkey)
        server_ephem_key = nacl.public.PrivateKey.generate()
        perfstats.keygens += 1
        xykey = nacl.public.Box(server_ephem_key, client_pubkey).shared_key()
        xbkey = nacl.public.Box(onion_privkey, client_pubkey).shared_key()
        perfstats.dhs += 2
        M = xykey + xbkey + \
                idpubkey + \
                onion_privkey.public_key.encode(encoder=nacl.encoding.RawEncoder) + \
                server_ephem_key.public_key.encode(encoder=nacl.encoding.RawEncoder)
        A = nacl.hash.sha256(M + b'verify', encoder=nacl.encoding.RawEncoder)
        S = nacl.hash.sha256(M + b'secret', encoder=nacl.encoding.RawEncoder)

        if sphinx_domainsep is not None:
            blindkey = Sphinx.makeblindkey(S, sphinx_domainsep, perfstats)
            blinded_client_pubkey = Sphinx.reblindpubkey(blindkey,
                    client_pubkey, perfstats)
            return ((bytes(server_ephem_key.public_key),
                    bytes(onion_privkey.public_key), A),
                    S), blinded_client_pubkey
        else:
            return ((bytes(server_ephem_key.public_key),
                    bytes(onion_privkey.public_key), A), S)

    def verify(self, reply, onion_pubkey, idpubkey, sphinx_domainsep=None):
        """The client calls this method to verify the ntor reply
        message, passing the onion and id public keys for the server
        it's expecting to be talking to.  If sphinx_domainsep is not
        None, also compute the reblinding key so that the client can
        reuse this same NTor object for the next server.  Returns the
        shared secret on success, or raises ValueError on failure."""
        server_ephem_pubkey, server_onion_pubkey, authtag = reply
        if type(idpubkey) is not bytes:
            idpubkey = bytes(idpubkey)
        if type(server_ephem_pubkey) is bytes:
            server_ephem_pubkey = nacl.public.PublicKey(server_ephem_pubkey)
        if type(server_onion_pubkey) is bytes:
            server_onion_pubkey = nacl.public.PublicKey(server_onion_pubkey)
        if type(onion_pubkey) is bytes:
            onion_pubkey = nacl.public.PublicKey(onion_pubkey)
        if onion_pubkey != server_onion_pubkey:
            raise ValueError("NTor onion pubkey mismatch")
        # We use the blinding keys if present; if they're not present
        # (because we're not in Single-Pass Walking Onions), the loops
        # are just empty anyway, so everything will work in the usual
        # unblinded way.
        reblinded_server_ephem_pubkey = server_ephem_pubkey
        for blindkey in self.blinding_keys:
            reblinded_server_ephem_pubkey = Sphinx.reblindpubkey(blindkey,
                    reblinded_server_ephem_pubkey, self.perfstats)
        xykey = nacl.public.Box(self.client_ephem_key,
                reblinded_server_ephem_pubkey).shared_key()
        reblinded_onion_pubkey = onion_pubkey
        for blindkey in self.blinding_keys:
            reblinded_onion_pubkey = Sphinx.reblindpubkey(blindkey,
                    reblinded_onion_pubkey, self.perfstats)
        xbkey = nacl.public.Box(self.client_ephem_key,
                reblinded_onion_pubkey).shared_key()
        self.perfstats.dhs += 2
        M = xykey + xbkey + \
                idpubkey + \
                onion_pubkey.encode(encoder=nacl.encoding.RawEncoder) + \
                server_ephem_pubkey.encode(encoder=nacl.encoding.RawEncoder)
        Acheck = nacl.hash.sha256(M + b'verify', encoder=nacl.encoding.RawEncoder)
        S = nacl.hash.sha256(M + b'secret', encoder=nacl.encoding.RawEncoder)
        if Acheck != authtag:
            raise ValueError("NTor auth mismatch")

        if sphinx_domainsep is not None:
            blindkey = Sphinx.makeblindkey(S, sphinx_domainsep,
                    self.perfstats)
            self.blinding_keys.append(blindkey)
        return S


class VanillaExtendCircuitHandler:
    """A handler for VanillaExtendCircuitCell cells.  It allocates a new
    circuit id on the Channel to the requested next hop, connects the
    existing and new circuits together, and forwards a
    VanillaCreateCircuitMsg to the next hop."""

    def received_cell(self, circhandler, cell):
        # Remove ourselves from handling a second
        # VanillaExtendCircuitCell on this circuit
        circhandler.replace_celltype_handler(simcell.VanillaExtendCircuitCell, None)

        # Allocate a new circuit id to the requested next hop
        channelmgr = circhandler.channel.channelmgr
        nexthopchannel = channelmgr.get_channel_to(cell.hopaddr)
        newcircid, newcirchandler = nexthopchannel.new_circuit()

        # Connect the existing and new circuits together
        circhandler.adjacent_circuit_handler = newcirchandler
        newcirchandler.adjacent_circuit_handler = circhandler

        # Set up a handler for when the VanillaCreatedCircuitCell comes
        # back
        newcirchandler.replace_celltype_handler(
                simcell.VanillaCreatedCircuitCell,
                VanillaCreatedRelayHandler())

        # Forward a VanillaCreateCircuitMsg to the next hop
        nexthopchannel.send_msg(
                simmsg.VanillaCreateCircuitMsg(newcircid, cell.ntor_request))


class TelescopingExtendCircuitHandler:
    """A handler for TelescopingExtendCircuitCell cells.  It allocates a new
    circuit id on the Channel to the requested next hop, connects the
    existing and new circuits together, and forwards a
    TelescopingCreateCircuitMsg to the next hop."""

    def __init__(self, relaypicker, current_relay_idkey):
        self.relaypicker = relaypicker
        self.current_relay_idkey = bytes(current_relay_idkey)

    def received_cell(self, circhandler, cell):
        # Remove ourselves from handling a second
        # TelescopingExtendCircuitCell on this circuit
        circhandler.replace_celltype_handler(simcell.TelescopingExtendCircuitCell, None)

        # Find the SNIP corresponding to the  index sent by the client
        next_snip = self.relaypicker.pick_relay_by_uniform_index(cell.idx)

        # Check to make sure that we aren't extending to ourselves. If we are,
        # close the circuit.
        if next_snip.snipdict["idkey"] == self.current_relay_idkey:
            logging.debug("Client requested extending the circuit to a relay already in the path; aborting. my circid: %s", str(circhandler.circid))
            circhandler.close()
            return


        # Allocate a new circuit id to the requested next hop
        channelmgr = circhandler.channel.channelmgr
        nexthopchannel = channelmgr.get_channel_to(next_snip.snipdict["addr"])
        newcircid, newcirchandler = nexthopchannel.new_circuit()

        # Connect the existing and new circuits together
        circhandler.adjacent_circuit_handler = newcirchandler
        newcirchandler.adjacent_circuit_handler = circhandler


        # Set up a handler for when the TelescopingCreatedCircuitCell comes
        # back
        newcirchandler.replace_celltype_handler(
                simcell.TelescopingCreatedCircuitCell,
                TelescopingCreatedRelayHandler(next_snip))

        # Forward a TelescopingCreateCircuitMsg to the next hop
        nexthopchannel.send_msg(
                simmsg.TelescopingCreateCircuitMsg(newcircid, cell.ntor_request))


class VanillaCreatedRelayHandler:
    """Handle a VanillaCreatedCircuitCell received by a _relay_ that
    recently received a VanillaExtendCircuitCell from a client, and so
    forwarded a VanillaCreateCircuitCell to the next hop."""

    def received_cell(self, circhandler, cell):
        # Remove ourselves from handling a second
        # VanillaCreatedCircuitCell on this circuit
        circhandler.replace_celltype_handler(simcell.VanillaCreatedCircuitCell, None)

        # Just forward a VanillaExtendedCircuitCell back towards the
        # client
        circhandler.adjacent_circuit_handler.send_cell(
            simcell.VanillaExtendedCircuitCell(cell.ntor_reply))


class TelescopingCreatedRelayHandler:
    """Handle a TelescopingCreatedCircuitCell received by a _relay_ that
    recently received a TelescopingExtendCircuitCell from a client, and so
    forwarded a TelescopingCreateCircuitCell to the next hop."""

    def __init__(self, next_snip):
        self.next_snip = next_snip

    def received_cell(self, circhandler, cell):
        logging.debug("Handle a TelescopingCreatedCircuit received by a relay")
        # Remove ourselves from handling a second
        # TelescopingCreatedCircuitCell on this circuit
        circhandler.replace_celltype_handler(simcell.TelescopingCreatedCircuitCell, None)

        # Just forward a TelescopingExtendedCircuitCell back towards the
        # client
        circhandler.adjacent_circuit_handler.send_cell(
            simcell.TelescopingExtendedCircuitCell(cell.ntor_reply, self.next_snip))


class SinglePassCreatedRelayHandler:
    """Handle a SinglePassCreatedCircuitCell received by a _relay_ that
    recently received a SinglePassCreateCircuitMsg from this relay."""

    def __init__(self, ntorreply, next_snip, vrf_output, createdkey):
        self.ntorreply = ntorreply
        self.next_snip = next_snip
        self.vrf_output = vrf_output
        self.createdkey = createdkey

    def received_cell(self, circhandler, cell):
        logging.debug("Handle a SinglePassCreatedCircuitCell received by a relay")
        # Remove ourselves from handling a second
        # SinglePassCreatedCircuitCell on this circuit
        circhandler.replace_celltype_handler(simcell.SinglePassCreatedCircuitCell, None)
        logging.debug("Sending a SinglePassCreatedCell after receiving one from %s", self.next_snip.snipdict['addr'])

        # Forward a SinglePassCreatedCircuitCell back towards the client
        circhandler.adjacent_circuit_handler.channel_send_cell(
            simcell.SinglePassCreatedCircuitCell(self.ntorreply,
                simcell.SinglePassCreatedEnc(self.createdkey, self.next_snip,
                self.vrf_output, cell)))


class CircuitHandler:
    """A class for managing sending and receiving encrypted cells on a
    particular circuit."""

    class NoCryptLayer:
        def encrypt_msg(self, message): return message
        def decrypt_msg(self, message): return message

    class CryptLayer:
        def __init__(self, enckey, deckey, next_layer):
            self.enckey = enckey
            self.deckey = deckey
            self.next_layer = next_layer
        def encrypt_msg(self, message):
            return self.next_layer.encrypt_msg(simcell.EncryptedCell(self.enckey, message))
        def decrypt_msg(self, message):
            return self.next_layer.decrypt_msg(message).decrypt(self.deckey)

    def __init__(self, channel, circid):
        self.channel = channel
        self.circid = circid
        # The list of relay descriptors that form the circuit so far
        # (client side only)
        self.circuit_descs = []
        # The dispatch table is indexed by type, and the values are
        # objects with received_cell(circhandler, cell) methods.
        self.cell_dispatch_table = dict()
        # The topmost crypt layer.  This is an object with
        # encrypt_msg(msg) and decrypt_msg(msg) methods that returns the
        # en/decrypted messages respectively.  Messages are encrypted
        # starting with the last layer that was added (the keys for the
        # furthest away relay in the circuit) and are decrypted starting
        # with the first layer that was added (the keys for the guard).
        self.crypt_layer = self.NoCryptLayer()
        # The adjacent CircuitHandler that's connected to this one.  If
        # we get a cell on one, we forward it to the other (if it's not
        # meant for us to handle directly)
        self.adjacent_circuit_handler = None
        # The function to call when this circuit closes
        self.closer = lambda: self.channel.circuithandlers.pop(circid)

    def close(self):
        """Close the circuit.  Sends a CloseCell on the circuit (and its
        adjacent circuit, if present) and closes both."""
        adjcirchandler = self.adjacent_circuit_handler
        self.adjacent_circuit_handler = None
        logging.debug("Closing circuit. circid: %s", str(self.circid))
        if adjcirchandler is not None:
            adjcirchandler.adjacent_circuit_handler = None
        self.closer()
        self.channel_send_cell(simcell.CloseCell())
        if adjcirchandler is not None:
            adjcirchandler.closer()
            adjcirchandler.channel_send_cell(simcell.CloseCell())

    def send_cell(self, cell):
        """Send a cell on this circuit, encrypting as needed."""
        self.channel_send_cell(self.crypt_layer.encrypt_msg(cell))

    def channel_send_cell(self, cell):
        """Send a cell on this circuit directly without encrypting it
        first."""
        self.channel.send_msg(simmsg.CircuitCellMsg(self.circid, cell))

    def received_cell(self, cell):
        """A cell has been received on this circuit.  Dispatch it
        according to its type."""
        if isinstance(cell, simcell.EncryptedCell):
            cell = self.crypt_layer.decrypt_msg(cell)

        logging.debug("CircuitHandler: %s received cell %s on circuit %d from %s" % (self.channel.channelmgr.myaddr, cell, self.circid, self.channel.peer.channelmgr.myaddr))

        # If it's still encrypted, it's for sure meant for forwarding to
        # our adjacent hop, which had better exist.
        if isinstance(cell, simcell.EncryptedCell):
            self.adjacent_circuit_handler.send_cell(cell)
        else:
            # This is a plaintext cell meant for us.  Handle it
            # according to the table.
            celltype = type(cell)
            if celltype in self.cell_dispatch_table:
                self.cell_dispatch_table[celltype].received_cell(self, cell)

            elif isinstance(cell, simcell.StringCell):
                # Default handler; just print the message in the cell
                logging.debug("CircuitHandler: %s received '%s' on circuit %d from %s" \
                        % (self.channel.channelmgr.myaddr, cell,
                            self.circid, self.channel.peer.channelmgr.myaddr))

            elif isinstance(cell, simcell.CloseCell):
                logging.debug("Received CloseCell on circuit %s", str(self.circid))
                # Forward the CloseCell (without encryption) to the
                # adjacent circuit, if any, and close both this and the
                # adjacent circuit
                adjcirchandler = self.adjacent_circuit_handler
                self.adjacent_circuit_handler = None
                if adjcirchandler is not None:
                    adjcirchandler.adjacent_circuit_handler = None
                self.closer()
                if adjcirchandler is not None:
                    adjcirchandler.closer()
                    adjcirchandler.channel_send_cell(cell)

            else:
                # I don't know how to handle this cell?
                raise ValueError("CircuitHandler: %s received unknown cell type %s on circuit %d from %s" \
                        % (self.channel.channelmgr.myaddr, cell,
                            self.circid, self.channel.peer.channelmgr.myaddr))

    def replace_celltype_handler(self, celltype, handler):
        """Add an object with a received_cell(circhandler, cell) method
        to the cell dispatch table.  It replaces anything that's already
        there.  Passing None as the handler removes the dispatcher for
        that cell type."""
        if handler is None:
            del self.cell_dispatch_table[celltype]
        else:
            self.cell_dispatch_table[celltype] = handler

    def add_crypt_layer(self, enckey, deckey):
        """Add a processing layer to this CircuitHandler so that cells
        we send will get encrypted with the first given key, and cells
        we receive will be decrypted with the other given key."""
        current_crypt_layer = self.crypt_layer
        self.crypt_layer = self.CryptLayer(enckey, deckey, current_crypt_layer)


class Channel(Connection):
    """A class representing a channel between a relay and either a
    client or a relay, transporting cells from various circuits."""
    def __init__(self):
        super().__init__()
        # The RelayChannelManager managing this Channel
        self.channelmgr = None
        # The Channel at the other end
        self.peer = None
        # The function to call when the connection closes
        self.closer = lambda: None
        # The next circuit id to use on this channel.  The party that
        # opened the channel uses even numbers; the receiving party uses
        # odd numbers.
        self.next_circid = None
        # A map for CircuitHandlers to use for each open circuit on the
        # channel
        self.circuithandlers = dict()

    def closed(self):
        # Close each circuithandler we're managing
        while self.circuithandlers:
            chitems = iter(self.circuithandlers.items())
            circid, circhandler = next(chitems)
            logging.debug('closing circuit %s', circid)
            circhandler.close()
        self.closer()
        self.peer = None

    def close(self):
        peer = self.peer
        self.closed()
        if peer is not None and peer is not self:
            peer.closed()

    def new_circuit(self):
        """Allocate a new circuit on this channel, returning the new
        circuit's id and the new CircuitHandler."""
        circid = self.next_circid
        self.next_circid += 2
        circuithandler = CircuitHandler(self, circid)
        self.circuithandlers[circid] = circuithandler
        return circid, circuithandler

    def is_circuit_open(self, circid):
        is_open = (circid in self.circuithandlers) and (self.circuithandlers[circid] is not None)
        return is_open

    def new_circuit_with_circid(self, circid):
        """Allocate a new circuit on this channel, with the circuit id
        received from our peer.  Return the new CircuitHandler"""
        circuithandler = CircuitHandler(self, circid)
        self.circuithandlers[circid] = circuithandler
        return circuithandler

    def send_cell(self, circid, cell):
        """Send the given message on the given circuit, encrypting or
        decrypting as needed."""
        self.circuithandlers[circid].send_cell(cell)

    def send_raw_cell(self, circid, cell):
        """Send the given message, tagged for the given circuit id. No
        encryption or decryption is done."""
        self.send_msg(simmsg.CircuitCellMsg(self.circid, self.cell))

    def send_msg(self, message):
        """Send the given NetMsg on the channel."""
        self.channelmgr.perfstats.bytes_sent += message.size()
        self.peer.received(self.channelmgr.myaddr, message)

    def received(self, peeraddr, message):
        """Callback when a message is received from the network."""
        logging.debug('Channel: %s received %s from %s' % (self.channelmgr.myaddr, message, peeraddr))
        self.channelmgr.perfstats.bytes_received += message.size()
        if isinstance(message, simmsg.CircuitCellMsg):
            circid, cell = message.circid, message.cell
            self.circuithandlers[circid].received_cell(cell)
        else:
            self.channelmgr.received_msg(message, peeraddr, self)


class ChannelManager:
    """The class that manages the channels to other relays and clients.
    Relays and clients both use subclasses of this class to both create
    on-demand channels to relays, to gracefully handle the closing of
    channels, and to handle commands received over the channels."""

    def __init__(self, myaddr, dirauthaddrs, perfstats):
        # A dictionary of Channels to other hosts, indexed by NetAddr
        self.channels = dict()
        self.myaddr = myaddr
        self.dirauthaddrs = dirauthaddrs
        self.consensus = None
        self.relaypicker = None
        self.perfstats = perfstats

    def terminate(self):
        """Close all connections we're managing."""
        while self.channels:
            channelitems = iter(self.channels.items())
            addr, channel = next(channelitems)
            logging.debug('closing channel %s %s', addr, channel)
            channel.close()

    def add_channel(self, channel, peeraddr):
        """Add the given channel to the list of channels we are
        managing.  If we are already managing a channel to the same
        peer, close it first."""
        if peeraddr in self.channels:
            self.channels[peeraddr].close()

        channel.channelmgr = self
        self.channels[peeraddr] = channel
        channel.closer = lambda: self.channels.pop(peeraddr)

    def get_channel_to(self, addr):
        """Get the Channel connected to the given NetAddr, creating one
        if none exists right now."""
        if addr in self.channels:
            return self.channels[addr]

        # Create the new channel
        logging.debug('getting channel from %s to %s',self.myaddr,addr)
        newchannel = network.thenetwork.connect(self.myaddr, addr,
                self.perfstats)
        logging.debug('got channel from %s to %s',self.myaddr,addr)
        self.channels[addr] = newchannel
        newchannel.closer = lambda: self.channels.pop(addr)
        newchannel.channelmgr = self

        return newchannel

    def received_msg(self, message, peeraddr, channel):
        """Callback when a NetMsg not specific to a circuit is
        received."""
        logging.debug("ChannelManager: Node %s received msg %s from %s" % (self.myaddr, message, peeraddr))

    def received_cell(self, circid, cell, peeraddr, channel):
        """Callback with a circuit-specific cell is received."""
        logging.debug("ChannelManager: Node %s received cell on circ %d: %s from %s" % (self.myaddr, circid, cell, peeraddr))

    def send_msg(self, message, peeraddr):
        """Send a message to the peer with the given address."""
        channel = self.get_channel_to(peeraddr)
        channel.send_msg(message)

    def send_cell(self, circid, cell, peeraddr):
        """Send a cell on the given circuit to the peer with the given
        address."""
        channel = self.get_channel_to(peeraddr)
        channel.send_cell(circid, cell)


class RelayChannelManager(ChannelManager):
    """The subclass of ChannelManager for relays."""

    def __init__(self, myaddr, dirauthaddrs, onionprivkey, idpubkey,
            desc_getter, path_selection_key_getter, perfstats):
        super().__init__(myaddr, dirauthaddrs, perfstats)
        self.onionkey = onionprivkey
        self.idpubkey = idpubkey
        if network.thenetwork.womode != network.WOMode.VANILLA:
            self.endive = None
        self.desc_getter = desc_getter

        if network.thenetwork.womode == network.WOMode.SINGLEPASS:
            self.path_selection_key_getter = path_selection_key_getter

    def get_consensus(self):
        """Download a fresh consensus (and ENDIVE if using Walking
        Onions) from a random dirauth."""
        a = random.choice(self.dirauthaddrs)
        c = network.thenetwork.connect(self, a, self.perfstats)
        if network.thenetwork.womode == network.WOMode.VANILLA:
            if self.consensus is not None and \
                    len(self.consensus.consdict['relays']) > 0:
                self.consensus = c.getconsensusdiff()
            else:
                self.consensus = c.getconsensus()

            logging.debug("Relay %s requests consensus from %s", self.myaddr, str(network.thenetwork.dirauthkeys()))
            self.relaypicker = dirauth.Consensus.verify(self.consensus,
                    network.thenetwork.dirauthkeys(), self.perfstats)
        else:
            self.consensus = c.getconsensus()
            if self.endive is not None and \
                    len(self.endive.enddict['snips']) > 0:
                self.endive = c.getendivediff()
            else:
                self.endive = c.getendive()
            self.relaypicker = dirauth.ENDIVE.verify(self.endive,
                    self.consensus, network.thenetwork.dirauthkeys(),
                    self.perfstats)
        c.close()

    def received_msg(self, message, peeraddr, channel):
        """Callback when a NetMsg not specific to a circuit is
        received."""
        logging.debug("RelayChannelManager: Node %s received msg %s from %s" % (self.myaddr, message, peeraddr))
        if isinstance(message, simmsg.RelayRandomHopMsg):
            if message.ttl > 0:
                # Pick a random next hop from the consensus
                nexthop = self.relaypicker.pick_weighted_relay()
                if network.thenetwork.womode == network.WOMode.VANILLA:
                    nextaddr = nexthop.descdict["addr"]
                else:
                    nextaddr = nexthop.snipdict["addr"]
                self.send_msg(simmsg.RelayRandomHopMsg(message.ttl-1), nextaddr)
        elif isinstance(message, simmsg.RelayGetConsensusMsg):
            self.send_msg(simmsg.RelayConsensusMsg(self.consensus), peeraddr)
        elif isinstance(message, simmsg.RelayGetConsensusDiffMsg):
            self.send_msg(simmsg.RelayConsensusDiffMsg(self.consensus), peeraddr)
        elif isinstance(message, simmsg.RelayGetDescMsg):
            self.send_msg(simmsg.RelayDescMsg(self.desc_getter()), peeraddr)
        elif isinstance(message, simmsg.VanillaCreateCircuitMsg):
            # A new circuit has arrived
            circhandler = channel.new_circuit_with_circid(message.circid)
            # Create the ntor reply
            reply, secret = NTor.reply(self.onionkey, self.idpubkey,
                    message.ntor_request, self.perfstats)
            # Set up the circuit to use the shared secret
            enckey = nacl.hash.sha256(secret + b'downstream')
            deckey = nacl.hash.sha256(secret + b'upstream')
            circhandler.add_crypt_layer(enckey, deckey)
            # Add a handler for if an Extend Cell arrives (there should
            # be at most one on this circuit).
            circhandler.replace_celltype_handler(
                    simcell.VanillaExtendCircuitCell, VanillaExtendCircuitHandler())
            # Send the ntor reply
            self.send_msg(simmsg.CircuitCellMsg(message.circid,
                    simcell.VanillaCreatedCircuitCell(reply)), peeraddr)
        elif isinstance(message, simmsg.TelescopingCreateCircuitMsg):
            # A new circuit has arrived
            circhandler = channel.new_circuit_with_circid(message.circid)
            # Create the ntor reply
            reply, secret = NTor.reply(self.onionkey, self.idpubkey,
                    message.ntor_request, self.perfstats)
            # Set up the circuit to use the shared secret
            enckey = nacl.hash.sha256(secret + b'downstream')
            deckey = nacl.hash.sha256(secret + b'upstream')
            circhandler.add_crypt_layer(enckey, deckey)
            # Add a handler for if an Extend Cell arrives (there should
            # be at most one on this circuit).
            circhandler.replace_celltype_handler(
                    simcell.TelescopingExtendCircuitCell,
                    TelescopingExtendCircuitHandler(self.relaypicker,
                        self.idpubkey))

            # Send the ntor reply
            self.send_msg(simmsg.CircuitCellMsg(message.circid,
                    simcell.TelescopingCreatedCircuitCell(reply)), peeraddr)
        elif isinstance(message, simmsg.SinglePassCreateCircuitMsg) and message.ttl == 0:
            # we are the end of the circuit, just establish a shared key and
            # return
            logging.debug("RelayChannelManager: Single-Pass TTL is 0, replying without extending")

            # A new circuit has arrived
            circhandler = channel.new_circuit_with_circid(message.circid)

            # Create the ntor reply
            reply, secret = NTor.reply(self.onionkey, self.idpubkey,
                message.ntor_request, self.perfstats)
            # Set up the circuit to use the shared secret
            enckey = nacl.hash.sha256(secret + b'downstream')
            deckey = nacl.hash.sha256(secret + b'upstream')
            circhandler.add_crypt_layer(enckey, deckey)

            # Send the ntor reply, but no need to send the snip for the next
            # relay or vrf proof, as this is the last relay in the circuit.
            self.send_msg(simmsg.CircuitCellMsg(message.circid,
                simcell.SinglePassCreatedCircuitCell(reply, None)), peeraddr)

        elif isinstance(message, simmsg.SinglePassCreateCircuitMsg) and message.ttl > 0:
            logging.debug("RelayChannelManager: Single-Pass TTL is greater than 0; extending")

            # A new circuit has arrived
            circhandler = channel.new_circuit_with_circid(message.circid)

            # Create the ntor reply for the circuit-extension key, and derive
            # the client's next blinded key
            (ntorreply, secret), blinded_client_encr_key = \
                    NTor.reply(self.onionkey, self.idpubkey,
                    message.ntor_request, self.perfstats,  b'circuit')

            # Set up the circuit to use the shared secret established from the
            # circuit extension key
            enckey = nacl.hash.sha256(secret + b'downstream')
            deckey = nacl.hash.sha256(secret + b'upstream')
            createdkey = nacl.hash.sha256(secret + b'created')

            # Here, we will directly extend the circuit ourselves, after
            # determining the next relay using the client's path selection
            # key in conjunction with our own
            pathsel_rand, blinded_client_path_selection_key = \
                    Sphinx.server(nacl.public.PublicKey(message.clipathselkey),
                    self.onionkey, b'pathsel', False, self.perfstats)

            pathselkey = self.path_selection_key_getter()
            # Simulate the VRF output for now (but it has the right
            # size, and charges the right number of group operations to
            # the perfstats)
            vrf_output = VRF.get_output(pathselkey, pathsel_rand,
                    self.perfstats)

            index = int.from_bytes(vrf_output[0][:4], 'big', signed=False)

            next_hop = self.relaypicker.pick_relay_by_uniform_index(index)
            if next_hop == None:
                logging.debug("Client requested extending the circuit to a relay index that results in None, aborting. my circid: %s", str(circhandler.circid))
                circhandler.close()
            elif next_hop.snipdict["idkey"] == bytes(self.idpubkey):
                logging.debug("Client requested extending the circuit to a relay already in the path; aborting. my circid: %s", str(circhandler.circid))
                circhandler.close()
                return

            # Allocate a new circuit id to the requested next hop
            channelmgr = circhandler.channel.channelmgr
            nexthopchannel = channelmgr.get_channel_to(next_hop.snipdict["addr"])
            newcircid, newcirchandler = nexthopchannel.new_circuit()

            # Connect the existing and new circuits together
            circhandler.adjacent_circuit_handler = newcirchandler
            newcirchandler.adjacent_circuit_handler = circhandler

            # Add a handler for once the next relay replies to say that the
            # circuit has been created
            newcirchandler.replace_celltype_handler(
                    simcell.SinglePassCreatedCircuitCell,
                    SinglePassCreatedRelayHandler(ntorreply, next_hop,
                        vrf_output, createdkey))

            # Send the next create message to the next hop
            nexthopchannel.send_msg(simmsg.SinglePassCreateCircuitMsg(newcircid,
                blinded_client_encr_key, blinded_client_path_selection_key,
                message.ttl-1))

            # Now set up the crypto
            circhandler.add_crypt_layer(enckey, deckey)
        else:
            return super().received_msg(message, peeraddr, channel)

    def received_cell(self, circid, cell, peeraddr, channel):
        """Callback with a circuit-specific cell is received."""
        logging.debug("RelayChannelManager: Node %s received cell on circ %d: %s from %s" % (self.myaddr, circid, cell, peeraddr))
        return super().received_cell(circid, cell, peeraddr, channel)


class Relay(network.Server):
    """The class representing an onion relay."""

    def __init__(self, dirauthaddrs, bw, flags):
        idkey = nacl.signing.SigningKey.generate()
        name = idkey.verify_key.encode(encoder=nacl.encoding.HexEncoder).decode("ascii")

        # Gather performance statistics
        self.perfstats = network.PerfStats(network.EntType.RELAY, bw)
        self.perfstats.is_bootstrapping = True

        # Create the identity and onion keys
        self.idkey = idkey
        self.onionkey = nacl.public.PrivateKey.generate()
        self.perfstats.keygens += 2
        self.name = name

        # Bind to the network to get a network address
        self.netaddr = network.thenetwork.bind(self)
        self.perfstats.name = "Relay at %s" % self.netaddr

        # Our bandwidth and flags
        self.bw = bw
        self.flags = flags

        # Register for epoch change notification
        network.thenetwork.wantepochticks(self, True, priority=True, end=True)
        network.thenetwork.wantepochticks(self, True, priority=True)

        self.current_desc = None
        self.next_desc = None

        # Create the path selection key for Single-Pass Walking Onions
        if network.thenetwork.womode == network.WOMode.SINGLEPASS:
            self.path_selection_key = nacl.public.PrivateKey.generate()
            self.next_path_selection_key = self.path_selection_key
            self.perfstats.keygens += 1
        else:
            self.path_selection_key = None

        # Create the RelayChannelManager connection manager
        self.channelmgr = RelayChannelManager(self.netaddr, dirauthaddrs,
                self.onionkey, self.idkey.verify_key,
                lambda: self.current_desc, lambda: self.path_selection_key,
                self.perfstats)

        # Initially, we're not a fallback relay
        self.is_fallbackrelay = False

        self.uploaddesc()


    def terminate(self):
        """Stop this relay."""

        if self.is_fallbackrelay:
            # Fallback relays must not (for now) terminate
            raise RelayFallbackTerminationError(self)

        # Stop listening for epoch ticks
        network.thenetwork.wantepochticks(self, False, priority=True, end=True)
        network.thenetwork.wantepochticks(self, False, priority=True)

        # Tell the dirauths we're going away
        self.uploaddesc(False)

        # Close connections to other relays
        self.channelmgr.terminate()

        # Stop listening to our own bound port
        self.close()


    def set_is_fallbackrelay(self, isfallback = True):
        """Set this relay to be a fallback relay (or unset if passed
        False)."""
        self.is_fallbackrelay = isfallback

    def epoch_ending(self, epoch):
        # Download the new consensus, which will have been created
        # already since the dirauths' epoch_ending callbacks happened
        # before the relays'.
        self.channelmgr.get_consensus()

    def newepoch(self, epoch):
        # Rotate the path selection key for Single-Pass Walking Onions
        if network.thenetwork.womode == network.WOMode.SINGLEPASS:
            self.path_selection_key = self.next_path_selection_key
            self.next_path_selection_key = nacl.public.PrivateKey.generate()
            self.perfstats.keygens += 1
        # Upload the descriptor for the *next* epoch (the one after the
        # one that just started)
        self.uploaddesc()

    def uploaddesc(self, upload=True):
        # Upload the descriptor for the epoch to come, or delete a
        # previous upload if upload=False
        descdict = dict()
        descdict["epoch"] = network.thenetwork.getepoch() + 1
        descdict["idkey"] = bytes(self.idkey.verify_key)
        descdict["onionkey"] = bytes(self.onionkey.public_key)
        descdict["addr"] = self.netaddr
        descdict["bw"] = self.bw
        descdict["flags"] = self.flags

        if network.thenetwork.womode == network.WOMode.SINGLEPASS:
            descdict["pathselkey"] = \
                    bytes(self.next_path_selection_key.public_key)

        desc = dirauth.RelayDescriptor(descdict)
        desc.sign(self.idkey, self.perfstats)
        dirauth.RelayDescriptor.verify(desc, self.perfstats)
        self.current_desc = self.next_desc
        self.next_desc = desc

        if upload:
            descmsg = simmsg.DirAuthUploadDescMsg(desc)
        else:
            # Note that this relies on signatures being deterministic;
            # otherwise we'd need to save the descriptor we uploaded
            # before so we could tell the airauths to delete the exact
            # one
            descmsg = simmsg.DirAuthDelDescMsg(desc)

        # Upload them
        for a in self.channelmgr.dirauthaddrs:
            c = network.thenetwork.connect(self, a, self.perfstats)
            c.sendmsg(descmsg)
            c.close()

    def connected(self, peer):
        """Callback invoked when someone (client or relay) connects to
        us.  Create a pair of linked Channels and return the peer half
        to the peer."""

        # Create the linked pair
        if peer is self.netaddr:
            # A self-loop?  We'll allow it.
            peerchannel = Channel()
            peerchannel.peer = peerchannel
            peerchannel.next_circid = 2
            return peerchannel

        peerchannel = Channel()
        ourchannel = Channel()
        peerchannel.peer = ourchannel
        peerchannel.next_circid = 2
        ourchannel.peer = peerchannel
        ourchannel.next_circid = 1

        # Add our channel to the RelayChannelManager
        self.channelmgr.add_channel(ourchannel, peer)

        return peerchannel
