def __learn_from_bootnode(self, bootnode): parsed_url = urlparse(bootnode.rest_url) # Pre-fetch certificate self.log.info("Fetching bootnode {} TLS certificate".format( bootnode.checksum_address)) bootnode_certificate = ssl.get_server_certificate( (parsed_url.hostname, parsed_url.port)) certificate = x509.load_pem_x509_certificate( bootnode_certificate.encode(), backend=default_backend()) # Write certificate filename = '{}.{}'.format(bootnode.checksum_address, Encoding.PEM.name.lower()) certificate_filepath = os.path.join(self.known_certificates_dir, filename) _write_tls_certificate(certificate=certificate, full_filepath=certificate_filepath, force=True) self.log.info("Saved bootnode {} TLS certificate".format( bootnode.checksum_address)) # Learn from Bootnode response = self.network_middleware.get_nodes_via_rest( url=parsed_url.netloc, certificate_filepath=certificate_filepath) self.log.info("Retrieved bootnode data from {}".format( bootnode.checksum_address)) if response.status_code != 200: raise RuntimeError("Bad response from bootnode {}".format( bootnode.rest_url)) signature, nodes = signature_splitter(response.content, return_remainder=True) node_list = Ursula.batch_from_bytes( nodes, federated_only=self.federated_only) # TODO: 466 self.log.debug("Learned from Bootnode {}|{}".format( bootnode.checksum_address, parsed_url.geturl())) for node in node_list: self.known_nodes.add(node) return node_list
def learn_from_teacher_node(self, eager=True): """ Sends a request to node_url to find out about known nodes. """ self._learning_round += 1 try: current_teacher = self.current_teacher_node() except self.NotEnoughTeachers as e: self.log.warning("Can't learn right now: {}".format(e.args[0])) return rest_url = current_teacher.rest_interface # TODO: Name this..? # TODO: Do we really want to try to learn about all these nodes instantly? # Hearing this traffic might give insight to an attacker. if VerifiableNode in self.__class__.__bases__: announce_nodes = [self] else: announce_nodes = None unresponsive_nodes = set() try: # TODO: Streamline path generation certificate_filepath = os.path.join( self.known_certificates_dir, current_teacher.certificate_filename) response = self.network_middleware.get_nodes_via_rest( url=rest_url, nodes_i_need=self._node_ids_to_learn_about_immediately, announce_nodes=announce_nodes, certificate_filepath=certificate_filepath) except requests.exceptions.ConnectionError as e: unresponsive_nodes.add(current_teacher) teacher_rest_info = current_teacher.rest_information()[0] # TODO: This error isn't necessarily "no repsonse" - let's maybe pass on the text of the exception here. self.log.info("No Response from teacher: {}:{}.".format( teacher_rest_info.host, teacher_rest_info.port)) self.cycle_teacher_node() return if response.status_code != 200: raise RuntimeError("Bad response from teacher: {} - {}".format( response, response.content)) signature, nodes = signature_splitter(response.content, return_remainder=True) # TODO: This doesn't make sense - a decentralized node can still learn about a federated-only node. from nucypher.characters.lawful import Ursula node_list = Ursula.batch_from_bytes(nodes, federated_only=self.federated_only) new_nodes = [] for node in node_list: if node.checksum_public_address in self.known_nodes or node.checksum_public_address == self.checksum_public_address: continue # TODO: 168 Check version and update if required. try: if eager: node.verify_node(self.network_middleware, accept_federated_only=self.federated_only) else: node.validate_metadata( accept_federated_only=self.federated_only) except node.SuspiciousActivity: # TODO: Account for possibility that stamp, rather than interface, was bad. message = "Suspicious Activity: Discovered node with bad signature: {}. " \ "Propagated by: {}".format(current_teacher.checksum_public_address, rest_url) self.log.warning(message) self.log.info("Previously unknown node: {}".format( node.checksum_public_address)) self.log.info("Previously unknown node: {}".format( node.checksum_public_address)) self.remember_node(node) new_nodes.append(node) self._adjust_learning(new_nodes) learning_round_log_message = "Learning round {}. Teacher: {} knew about {} nodes, {} were new." self.log.info( learning_round_log_message.format( self._learning_round, current_teacher.checksum_public_address, len(node_list), len(new_nodes)), ) if new_nodes and self.known_certificates_dir: for node in new_nodes: node.save_certificate_to_disk(self.known_certificates_dir) return new_nodes
def learn_from_teacher_node(self, eager=True): """ Sends a request to node_url to find out about known nodes. """ self._learning_round += 1 try: current_teacher = self.current_teacher_node() except self.NotEnoughTeachers as e: self.log.warn("Can't learn right now: {}".format(e.args[0])) return if Teacher in self.__class__.__bases__: announce_nodes = [self] else: announce_nodes = None unresponsive_nodes = set() try: # TODO: Streamline path generation certificate_filepath = self.node_storage.generate_certificate_filepath( checksum_address=current_teacher.checksum_public_address) response = self.network_middleware.get_nodes_via_rest( node=current_teacher, nodes_i_need=self._node_ids_to_learn_about_immediately, announce_nodes=announce_nodes, fleet_checksum=self.known_nodes.checksum) except NodeSeemsToBeDown as e: unresponsive_nodes.add(current_teacher) self.log.info("Bad Response from teacher: {}:{}.".format( current_teacher, e)) return finally: self.cycle_teacher_node() # # Before we parse the response, let's handle some edge cases. if response.status_code == 204: # In this case, this node knows about no other nodes. Hopefully we've taught it something. if response.content == b"": return NO_KNOWN_NODES # In the other case - where the status code is 204 but the repsonse isn't blank - we'll keep parsing. # It's possible that our fleet states match, and we'll check for that later. elif response.status_code != 200: self.log.info("Bad response from teacher {}: {} - {}".format( current_teacher, response, response.content)) return try: signature, node_payload = signature_splitter(response.content, return_remainder=True) except BytestringSplittingError as e: self.log.warn(e.args[0]) return try: self.verify_from(current_teacher, node_payload, signature=signature) except current_teacher.InvalidSignature: # TODO: What to do if the teacher improperly signed the node payload? raise # End edge case handling. # fleet_state_checksum_bytes, fleet_state_updated_bytes, node_payload = FleetStateTracker.snapshot_splitter( node_payload, return_remainder=True) current_teacher.last_seen = maya.now() # TODO: This is weird - let's get a stranger FleetState going. checksum = fleet_state_checksum_bytes.hex() # TODO: This doesn't make sense - a decentralized node can still learn about a federated-only node. from nucypher.characters.lawful import Ursula if constant_or_bytes(node_payload) is FLEET_STATES_MATCH: current_teacher.update_snapshot( checksum=checksum, updated=maya.MayaDT( int.from_bytes(fleet_state_updated_bytes, byteorder="big")), number_of_known_nodes=len(self.known_nodes)) return FLEET_STATES_MATCH node_list = Ursula.batch_from_bytes( node_payload, federated_only=self.federated_only) # TODO: 466 current_teacher.update_snapshot(checksum=checksum, updated=maya.MayaDT( int.from_bytes( fleet_state_updated_bytes, byteorder="big")), number_of_known_nodes=len(node_list)) new_nodes = [] for node in node_list: if GLOBAL_DOMAIN not in self.learning_domains: if not self.learning_domains.intersection( node.serving_domains): continue # This node is not serving any of our domains. # First, determine if this is an outdated representation of an already known node. with suppress(KeyError): already_known_node = self.known_nodes[ node.checksum_public_address] if not node.timestamp > already_known_node.timestamp: self.log.debug("Skipping already known node {}".format( already_known_node)) # This node is already known. We can safely continue to the next. continue certificate_filepath = self.node_storage.store_node_certificate( certificate=node.certificate) try: if eager: node.verify_node( self.network_middleware, accept_federated_only=self.federated_only, # TODO: 466 certificate_filepath=certificate_filepath) self.log.debug("Verified node: {}".format( node.checksum_public_address)) else: node.validate_metadata( accept_federated_only=self.federated_only) # TODO: 466 # This block is a mess of eagerness. This can all be done better lazily. except NodeSeemsToBeDown as e: self.log.info( f"Can't connect to {node} to verify it right now.") except node.InvalidNode: # TODO: Account for possibility that stamp, rather than interface, was bad. self.log.warn(node.invalid_metadata_message.format(node)) except node.SuspiciousActivity: message = "Suspicious Activity: Discovered node with bad signature: {}. " \ "Propagated by: {}".format(current_teacher.checksum_public_address, teacher_uri) self.log.warn(message) else: new = self.remember_node(node, record_fleet_state=False) if new: new_nodes.append(node) self._adjust_learning(new_nodes) learning_round_log_message = "Learning round {}. Teacher: {} knew about {} nodes, {} were new." self.log.info( learning_round_log_message.format(self._learning_round, current_teacher, len(node_list), len(new_nodes)), ) if new_nodes: self.known_nodes.record_fleet_state() for node in new_nodes: self.node_storage.store_node_certificate( certificate=node.certificate) return new_nodes