Esempio n. 1
0
    def __init__(self, loop: asyncio.BaseEventLoop, peer_manager: 'PeerManager',
                 routing_table: 'TreeRoutingTable', protocol: 'KademliaProtocol', key: bytes,
                 bottom_out_limit: typing.Optional[int] = 2, max_results: typing.Optional[int] = constants.k,
                 exclude: typing.Optional[typing.List[typing.Tuple[str, int]]] = None,
                 shortlist: typing.Optional[typing.List['KademliaPeer']] = None):
        if len(key) != constants.hash_length:
            raise ValueError("invalid key length: %i" % len(key))
        self.loop = loop
        self.peer_manager = peer_manager
        self.routing_table = routing_table
        self.protocol = protocol

        self.key = key
        self.bottom_out_limit = bottom_out_limit
        self.max_results = max_results
        self.exclude = exclude or []

        self.shortlist: typing.List['KademliaPeer'] = get_shortlist(routing_table, key, shortlist)
        self.active: typing.Set['KademliaPeer'] = set()
        self.contacted: typing.Set[typing.Tuple[str, int]] = set()
        self.distance = Distance(key)

        self.closest_peer: typing.Optional['KademliaPeer'] = None if not self.shortlist else self.shortlist[0]
        self.prev_closest_peer: typing.Optional['KademliaPeer'] = None

        self.iteration_queue = asyncio.Queue(loop=self.loop)

        self.running_probes: typing.Set[asyncio.Task] = set()
        self.iteration_count = 0
        self.bottom_out_count = 0
        self.running = False
        self.tasks: typing.List[asyncio.Task] = []
        self.delayed_calls: typing.List[asyncio.Handle] = []
Esempio n. 2
0
    def test_invalid_key_length(self):
        self.assertRaises(ValueError, Distance, b'1' * 47)
        self.assertRaises(ValueError, Distance, b'1' * 49)
        self.assertRaises(ValueError, Distance, b'')

        self.assertRaises(ValueError, Distance(b'0' * 48), b'1' * 47)
        self.assertRaises(ValueError, Distance(b'0' * 48), b'1' * 49)
        self.assertRaises(ValueError, Distance(b'0' * 48), b'')
Esempio n. 3
0
 def should_split(self, bucket_index: int, to_add: bytes) -> bool:
     #  https://stackoverflow.com/questions/32129978/highly-unbalanced-kademlia-routing-table/32187456#32187456
     if bucket_index < self._split_buckets_under_index:
         return True
     contacts = self.get_peers()
     distance = Distance(self._parent_node_id)
     contacts.sort(key=lambda c: distance(c.node_id))
     kth_contact = contacts[-1] if len(contacts) < constants.k else contacts[constants.k - 1]
     return distance(to_add) < distance(kth_contact.node_id)
Esempio n. 4
0
 async def peer_search(self, node_id: bytes, count=constants.k, max_results=constants.k*2,
                       bottom_out_limit=20) -> typing.List['KademliaPeer']:
     accumulated: typing.List['KademliaPeer'] = []
     async with self.peer_search_junction(node_id, max_results=max_results,
                                          bottom_out_limit=bottom_out_limit) as junction:
         async for peers in junction:
             accumulated.extend(peers)
     distance = Distance(node_id)
     accumulated.sort(key=lambda peer: distance(peer.node_id))
     return accumulated[:count]
Esempio n. 5
0
 def find_close_peers(self, key: bytes, count: typing.Optional[int] = None,
                      sender_node_id: typing.Optional[bytes] = None) -> typing.List['KademliaPeer']:
     exclude = [self._parent_node_id]
     if sender_node_id:
         exclude.append(sender_node_id)
     count = count or constants.k
     distance = Distance(key)
     contacts = self.get_peers()
     contacts = [c for c in contacts if c.node_id not in exclude]
     if contacts:
         contacts.sort(key=lambda c: distance(c.node_id))
         return contacts[:min(count, len(contacts))]
     return []
Esempio n. 6
0
    def get_peers(self,
                  count=-1,
                  exclude_contact=None,
                  sort_distance_to=None) -> typing.List['KademliaPeer']:
        """ Returns a list containing up to the first count number of contacts

        @param count: The amount of contacts to return (if 0 or less, return
                      all contacts)
        @type count: int
        @param exclude_contact: A node node_id to exclude; if this contact is in
                               the list of returned values, it will be
                               discarded before returning. If a C{str} is
                               passed as this argument, it must be the
                               contact's ID.
        @type exclude_contact: str

        @param sort_distance_to: Sort distance to the node_id, defaulting to the parent node node_id. If False don't
                                 sort the contacts

        @raise IndexError: If the number of requested contacts is too large

        @return: Return up to the first count number of contacts in a list
                If no contacts are present an empty is returned
        @rtype: list
        """
        peers = [
            peer for peer in self.peers if peer.node_id != exclude_contact
        ]

        # Return all contacts in bucket
        if count <= 0:
            count = len(peers)

        # Get current contact number
        current_len = len(peers)

        # If count greater than k - return only k contacts
        if count > constants.k:
            count = constants.k

        if not current_len:
            return peers

        if sort_distance_to is False:
            pass
        else:
            sort_distance_to = sort_distance_to or self._node_id
            peers.sort(key=lambda c: Distance(sort_distance_to)(c.node_id))

        return peers[:min(current_len, count)]
Esempio n. 7
0
 def __init__(self, peer_manager: 'PeerManager', range_min: int, range_max: int, node_id: bytes):
     """
     @param range_min: The lower boundary for the range in the n-bit ID
                      space covered by this k-bucket
     @param range_max: The upper boundary for the range in the ID space
                      covered by this k-bucket
     """
     self._peer_manager = peer_manager
     self.last_accessed = 0
     self.range_min = range_min
     self.range_max = range_max
     self.peers: typing.List['KademliaPeer'] = []
     self._node_id = node_id
     self._distance_to_self = Distance(node_id)
Esempio n. 8
0
def get_shortlist(routing_table: 'TreeRoutingTable', key: bytes,
                  shortlist: typing.Optional[typing.List['KademliaPeer']]) -> typing.List['KademliaPeer']:
    """
    If not provided, initialize the shortlist of peers to probe to the (up to) k closest peers in the routing table

    :param routing_table: a TreeRoutingTable
    :param key: a 48 byte hash
    :param shortlist: optional manually provided shortlist, this is done during bootstrapping when there are no
                      peers in the routing table. During bootstrap the shortlist is set to be the seed nodes.
    """
    if len(key) != constants.hash_length:
        raise ValueError("invalid key length: %i" % len(key))
    if not shortlist:
        shortlist = routing_table.find_close_peers(key)
    distance = Distance(key)
    shortlist.sort(key=lambda peer: distance(peer.node_id), reverse=True)
    return shortlist
Esempio n. 9
0
 async def peer_search(
     self,
     node_id: bytes,
     count=constants.k,
     max_results=constants.k * 2,
     bottom_out_limit=20,
     shortlist: typing.Optional[typing.List['KademliaPeer']] = None
 ) -> typing.List['KademliaPeer']:
     peers = []
     async for iteration_peers in self.get_iterative_node_finder(
             node_id,
             shortlist=shortlist,
             bottom_out_limit=bottom_out_limit,
             max_results=max_results):
         peers.extend(iteration_peers)
     distance = Distance(node_id)
     peers.sort(key=lambda peer: distance(peer.node_id))
     return peers[:count]
Esempio n. 10
0
class IterativeFinder:
    def __init__(
            self,
            loop: asyncio.BaseEventLoop,
            peer_manager: 'PeerManager',
            routing_table: 'TreeRoutingTable',
            protocol: 'KademliaProtocol',
            key: bytes,
            bottom_out_limit: typing.Optional[int] = 2,
            max_results: typing.Optional[int] = constants.k,
            exclude: typing.Optional[typing.List[typing.Tuple[str,
                                                              int]]] = None,
            shortlist: typing.Optional[typing.List['KademliaPeer']] = None):
        if len(key) != constants.hash_length:
            raise ValueError("invalid key length: %i" % len(key))
        self.loop = loop
        self.peer_manager = peer_manager
        self.routing_table = routing_table
        self.protocol = protocol

        self.key = key
        self.bottom_out_limit = bottom_out_limit
        self.max_results = max_results
        self.exclude = exclude or []

        self.shortlist: typing.List['KademliaPeer'] = get_shortlist(
            routing_table, key, shortlist)
        self.active: typing.Set['KademliaPeer'] = set()
        self.contacted: typing.Set[typing.Tuple[str, int]] = set()
        self.distance = Distance(key)

        self.closest_peer: typing.Optional[
            'KademliaPeer'] = None if not self.shortlist else self.shortlist[0]
        self.prev_closest_peer: typing.Optional['KademliaPeer'] = None

        self.iteration_queue = asyncio.Queue(loop=self.loop)

        self.running_probes: typing.Set[asyncio.Task] = set()
        self.iteration_count = 0
        self.bottom_out_count = 0
        self.running = False
        self.tasks: typing.List[asyncio.Task] = []
        self.delayed_calls: typing.List[asyncio.Handle] = []

    async def send_probe(self, peer: 'KademliaPeer') -> FindResponse:
        """
        Send the rpc request to the peer and return an object with the FindResponse interface
        """
        raise NotImplementedError()

    def search_exhausted(self):
        """
        This method ends the iterator due no more peers to contact.
        Override to provide last time results.
        """
        self.iteration_queue.put_nowait(None)

    def check_result_ready(self, response: FindResponse):
        """
        Called after adding peers from an rpc result to the shortlist.
        This method is responsible for putting a result for the generator into the Queue
        """
        raise NotImplementedError()

    def get_initial_result(self) -> typing.List['KademliaPeer']:
        """
        Get an initial or cached result to be put into the Queue. Used for findValue requests where the blob
        has peers in the local data store of blobs announced to us
        """
        return []

    def _is_closer(self, peer: 'KademliaPeer') -> bool:
        return not self.closest_peer or self.distance.is_closer(
            peer.node_id, self.closest_peer.node_id)

    def _update_closest(self):
        self.shortlist.sort(key=lambda peer: self.distance(peer.node_id),
                            reverse=True)
        if self.closest_peer and self.closest_peer is not self.shortlist[-1]:
            if self._is_closer(self.shortlist[-1]):
                self.prev_closest_peer = self.closest_peer
                self.closest_peer = self.shortlist[-1]

    async def _handle_probe_result(self, peer: 'KademliaPeer',
                                   response: FindResponse):
        if peer not in self.shortlist:
            self.shortlist.append(peer)
        if peer not in self.active:
            self.active.add(peer)
        for contact_triple in response.get_close_triples():
            node_id, address, udp_port = contact_triple
            if (
                    address, udp_port
            ) not in self.contacted:  # and not self.peer_manager.is_ignored(addr_tuple)
                found_peer = self.peer_manager.get_kademlia_peer(
                    node_id, address, udp_port)
                if found_peer not in self.shortlist and self.peer_manager.peer_is_good(
                        peer) is not False:
                    self.shortlist.append(found_peer)
        self._update_closest()
        self.check_result_ready(response)

    async def _send_probe(self, peer: 'KademliaPeer'):
        try:
            response = await self.send_probe(peer)
        except asyncio.TimeoutError:
            self.active.discard(peer)
            return
        except ValueError as err:
            log.warning(str(err))
            self.active.discard(peer)
            return
        except (RemoteException, TransportNotConnected):
            return
        return await self._handle_probe_result(peer, response)

    async def _search_round(self):
        """
        Send up to constants.alpha (5) probes to the closest peers in the shortlist
        """

        added = 0
        self.shortlist.sort(key=lambda p: self.distance(p.node_id),
                            reverse=True)
        while self.running and len(self.shortlist) and added < constants.alpha:
            peer = self.shortlist.pop()
            origin_address = (peer.address, peer.udp_port)
            if origin_address in self.exclude or self.peer_manager.peer_is_good(
                    peer) is False:
                continue
            if peer.node_id == self.protocol.node_id:
                continue
            if (peer.address, peer.udp_port) == (self.protocol.external_ip,
                                                 self.protocol.udp_port):
                continue
            if (peer.address, peer.udp_port) not in self.contacted:
                self.contacted.add((peer.address, peer.udp_port))

                t = self.loop.create_task(self._send_probe(peer))

                def callback(_):
                    self.running_probes.difference_update({
                        probe
                        for probe in self.running_probes
                        if probe.done() or probe == t
                    })
                    if not self.running_probes and self.shortlist:
                        self.tasks.append(
                            self.loop.create_task(self._search_task(0.0)))

                t.add_done_callback(callback)
                self.running_probes.add(t)
                added += 1
        log.debug("running %d probes", len(self.running_probes))
        if not added and not self.running_probes:
            log.debug("search for %s exhausted", hexlify(self.key)[:8])
            self.search_exhausted()

    async def _search_task(
            self,
            delay: typing.Optional[float] = constants.iterative_lookup_delay):
        try:
            if self.running:
                await self._search_round()
            if self.running:
                self.delayed_calls.append(
                    self.loop.call_later(delay, self._search))
        except (asyncio.CancelledError, StopAsyncIteration,
                TransportNotConnected):
            if self.running:
                self.loop.call_soon(self.aclose)

    def _search(self):
        self.tasks.append(self.loop.create_task(self._search_task()))

    def __aiter__(self):
        if self.running:
            raise Exception("already running")
        self.running = True
        self._search()
        return self

    async def __anext__(self) -> typing.List['KademliaPeer']:
        try:
            if self.iteration_count == 0:
                result = self.get_initial_result(
                ) or await self.iteration_queue.get()
            else:
                result = await self.iteration_queue.get()
            if not result:
                raise StopAsyncIteration
            self.iteration_count += 1
            return result
        except (asyncio.CancelledError, StopAsyncIteration):
            self.loop.call_soon(self.aclose)
            raise

    def aclose(self):
        self.running = False
        self.iteration_queue.put_nowait(None)
        for task in chain(self.tasks, self.running_probes, self.delayed_calls):
            task.cancel()
        self.tasks.clear()
        self.running_probes.clear()
        self.delayed_calls.clear()
Esempio n. 11
0
 def midpoint_id_in_bucket_range(self, bucket_index: int) -> bytes:
     half = int((self.buckets[bucket_index].range_max - self.buckets[bucket_index].range_min) // 2)
     return Distance(self._parent_node_id)(
         int(self.buckets[bucket_index].range_min + half).to_bytes(constants.hash_length, 'big')
     ).to_bytes(constants.hash_length, 'big')
Esempio n. 12
0
 def random_id_in_bucket_range(self, bucket_index: int) -> bytes:
     random_id = int(random.randrange(self.buckets[bucket_index].range_min, self.buckets[bucket_index].range_max))
     return Distance(
         self._parent_node_id
     )(random_id.to_bytes(constants.hash_length, 'big')).to_bytes(constants.hash_length, 'big')