def __init__(self, port, id_, version): self._id = id_ self._version = version self._server = KRPCServer(port, self._version) self._rt = FlatRoutingTable() # Thread details self._shutdown_flag = False self._thread = None # default handler self.handler = self.default_handler # Behaviour configuration # Am I actively seeking out other nodes? self.active_discovery = True # After how many seconds should i do another self-lookup? self.self_find_delay = 180.0 # How many active node discovery attempts between self-lookups? self.active_discoveries = 10 # Session key self._key = os.urandom(20) # 20 random bytes == 160 bits
class DHT(object): def __init__(self, port, id_, version): self._id = id_ self._version = version self._server = KRPCServer(port, self._version) self._rt = FlatRoutingTable() # Thread details self._shutdown_flag = False self._thread = None # default handler self.handler = self.default_handler # Behaviour configuration # Am I actively seeking out other nodes? self.active_discovery = True # After how many seconds should i do another self-lookup? self.self_find_delay = 180.0 # How many active node discovery attempts between self-lookups? self.active_discoveries = 10 # Session key self._key = os.urandom(20) # 20 random bytes == 160 bits def _get_id(self, target): # Retrieve ID to use to communicate with target node return self._id def start(self): """ Start the DHT node """ self._server.start() self._server.handler = self.handler # Add the default nodes #DEFAULT_CONNECT_INFO = ('67.215.242.139', 6881) DEFAULT_CONNECT_INFO = (socket.gethostbyaddr("router.bittorrent.com")[2][0], 6881) DEFAULT_NODE = Node(DEFAULT_CONNECT_INFO) DEFAULT_ID = self._server.ping(os.urandom(20), DEFAULT_NODE)['id'] self._rt.update_entry(DEFAULT_ID, DEFAULT_NODE) # Start our event thread self._thread = threading.Thread(target=self._pump) self._thread.daemon = True self._thread.start() def shutdown(self): self._server.shutdown() def __enter__(self): self.start() def __exit__(self, type_, value, traceback): self.shutdown() def _pump(self): """ Thread that maintains DHT connectivity and does routing table housekeeping. Started by self.start() The very first thing this function does, is look up itself in the DHT. This connects it to neighbouring nodes and enables it to give reasonable answers to incoming queries. Afterward we look up random nodes to increase our connectedness and gather information about the DHT as a whole """ # Try to establish links to close nodes logger.info("Establishing connections to DHT") self.find_node(self._id) delay = self.self_find_delay if self.active_discovery: delay /= (self.active_discoveries + 1) iteration = 0 while True: try: time.sleep(delay) iteration += 1 if self.active_discovery and iteration % (self.active_discoveries + 1) != 0: target = hashlib.sha1("this is my salt 2348724" + str(iteration) + self._id).digest() self.find_node(target) logger.info("Tracing done, routing table contains %d nodes", self._rt.node_count()) else: # Regular maintenance: # Find N random nodes. Execute a find_node() on them. # toss them if they come up empty. n = self._rt.sample(self._id, 10, 1) for node_id, c in n: try: r = self._server.find_node(self._id, c, self._id) if "nodes" in r: self._process_incoming_nodes(r["nodes"]) except KRPCTimeout: # The node did not reply. # Blacklist it. self._rt.bad_node(node_id, c) logger.info("Cleanup, routing table contains %d nodes", self._rt.node_count()) except: # This loop should run forever. If we get into trouble, log # the exception and carry on. logger.critical("Exception in DHT maintenance thread:\n\n" + traceback.format_exc()) def _process_incoming_nodes(self, bnodes): # Add them to the routing table for node_id, node_c in decode_nodes(bnodes): print (node_id, node_c) self._rt.update_entry(node_id, Node(node_c)) def _recurse(self, target, function, max_attempts=10, result_key=None): """ Recursively query the DHT, following "nodes" replies until we hit the desired key This is the workhorse function used by all recursive queries. """ logger.debug("Recursing to target %r" % target.encode("hex")) attempts = 0 while attempts < max_attempts: #print self._rt._nodes #print self._rt.get_close_nodes(target) attempts += 1 for id_, node in self._rt.get_close_nodes(target): try: r = function(self._get_id(id_), node, target) logger.debug("Recursion results from %r ", node.c) if result_key and result_key in r: return r[result_key] if "nodes" in r: self._process_incoming_nodes(r["nodes"]) except KRPCTimeout: # The node did not reply. # Blacklist it. print "BAAAAD" attempts += 1 self._rt.bad_node(id_, node) except KRPCError: # Sometimes we just flake out due to UDP being unreliable # Don't sweat it, just log and carry on. logger.error("KRPC Error:\n\n" + traceback.format_exc()) if result_key: # We were expecting a result, but we did not find it! # Raise the NotFoundError exception instead of returning None raise NotFoundError def find_node(self, target, attempts=10): """ Recursively call the find_node function to get as close as possible to the target node """ logger.debug("Tracing to %r" % target.encode("hex")) self._recurse(target, self._server.find_node, max_attempts=attempts) def get_peers(self, info_hash, attempts=10): """ Recursively call the get_peers function to fidn peers for the given info_hash """ logger.debug("Finding peers for %r" % info_hash.encode("hex")) return self._recurse(info_hash, self._server.get_peers, result_key="values", max_attempts=attempts) def default_handler(self, rec, c): """ Process incoming requests """ logger.info("REQUEST: %r %r" % (c, rec)) # Use the request to update the routing table peer_id = rec["a"]["id"] #print peer_id.encode('base64'), self._get_id(peer_id).encode('base64') if self._get_id(peer_id) == peer_id: # don't talk to yourself. return self._rt.update_entry(peer_id, Node(c)) # Skeleton response resp = {"y": "r", "t": rec["t"], "r": {"id": self._get_id(peer_id)}, "v": self._version} if rec["q"] == "ping": self._server.send_krpc_reply(resp, c) elif rec["q"] == "find_node": target = rec["a"]["target"] resp["r"]["id"] = self._get_id(target) resp["r"]["nodes"] = encode_nodes(self._rt.get_close_nodes(target)) self._server.send_krpc_reply(resp, c) elif rec["q"] == "get_peers": # Provide a token so we can receive announces # The token is generated using HMAC and a secret # session key, so we don't have to remember it. # Token is based on nodes id, connection details # torrent infohash to avoid clashes in NAT scenarios. info_hash = rec["a"]["info_hash"] resp["r"]["id"] = self._get_id(info_hash) token = hmac.new(self._key, info_hash + peer_id + str(c), hashlib.sha1).digest() resp["r"]["token"] = token # We don't actually keep any peer administration, so we # always send back the closest nodes resp["r"]["nodes"] = encode_nodes(self._rt.get_close_nodes(info_hash)) self._server.send_krpc_reply(resp, c) elif rec["q"] == "announce_peer": # First things first, validate the token. info_hash = rec["a"]["info_hash"] resp["r"]["id"] = self._get_id(info_hash) peer_id = rec["a"]["id"] token = hmac.new(self._key, info_hash + peer_id + str(c), hashlib.sha1).digest() if token != rec["a"]["token"]: return # Ignore the request else: # We don't actually keep any peer administration, so we # just acknowledge. self._server.send_krpc_reply(resp, c) else: logger.error("Unknown request in query %r" % rec)