From 6cbe8e5f3db6541c4a54e9ed69c705ebdc52b055 Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Thu, 22 Nov 2018 10:28:33 -0800 Subject: [PATCH] Organize Learner / Server methods; Recompose after diffusing cli utilities. --- nucypher/network/nodes.py | 286 +++++++++++++++++++--------------- nucypher/network/protocols.py | 26 ++++ nucypher/network/server.py | 2 + 3 files changed, 191 insertions(+), 123 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 62bba2a53..4ea2129ea 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -725,10 +725,127 @@ class Teacher: Raise when a Character tries to use another Character as decentralized when the latter is federated_only. """ - def seed_node_metadata(self): - return SeednodeMetadata(self.checksum_public_address, - self.rest_server.rest_interface.host, - self.rest_server.rest_interface.port) + # + # Alternate Constructors + # + + @classmethod + def from_seednode_metadata(cls, + seednode_metadata, + *args, + **kwargs): + """ + Essentially another deserialization method, but this one doesn't reconstruct a complete + node from bytes; instead it's just enough to connect to and verify a node. + """ + + return cls.from_seed_and_stake_info(checksum_address=seednode_metadata.checksum_address, + host=seednode_metadata.rest_host, + port=seednode_metadata.rest_port, + *args, **kwargs) + + @classmethod + def from_teacher_uri(cls, + federated_only: bool, + teacher_uri: str, + min_stake: int, + ) -> 'Ursula': + + hostname, port, checksum_address = parse_node_uri(uri=teacher_uri) + try: + teacher = cls.from_seed_and_stake_info(host=hostname, + port=port, + federated_only=federated_only, + checksum_address=checksum_address, + minimum_stake=min_stake) + + except (socket.gaierror, requests.exceptions.ConnectionError, ConnectionRefusedError): + # self.log.warn("Can't connect to seed node. Will retry.") + time.sleep(5) # TODO: Move this 5 + + else: + return teacher + + @classmethod + @validate_checksum_address + def from_seed_and_stake_info(cls, + host: str, + port: int, + federated_only: bool, + minimum_stake: int = 0, + checksum_address: str = None, + network_middleware: RestMiddleware = None, + *args, + **kwargs + ) -> 'Teacher': + + # + # WARNING: xxx Poison xxx + # Let's learn what we can about the ... "seednode". + # + + if network_middleware is None: + network_middleware = RestMiddleware() + + # Fetch the hosts TLS certificate and read the common name + certificate = network_middleware.get_certificate(host=host, port=port) + real_host = certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value + temp_node_storage = ForgetfulNodeStorage(federated_only=federated_only) + certificate_filepath = temp_node_storage.store_host_certificate(host=real_host, + certificate=certificate) + # Load the host as a potential seed node + potential_seed_node = cls.from_rest_url( + host=real_host, + port=port, + network_middleware=network_middleware, + certificate_filepath=certificate_filepath, + federated_only=True, + *args, + **kwargs) # TODO: 466 + + if checksum_address: + # Ensure this is the specific node we expected + if not checksum_address == potential_seed_node.checksum_public_address: + template = "This seed node has a different wallet address: {} (expected {}). Are you sure this is a seednode?" + raise potential_seed_node.SuspiciousActivity(template.format(potential_seed_node.checksum_public_address, + checksum_address)) + + # Check the node's stake (optional) + if minimum_stake > 0: + # TODO: check the blockchain to verify that address has more then minimum_stake. #511 + raise NotImplementedError("Stake checking is not implemented yet.") + + # Verify the node's TLS certificate + try: + potential_seed_node.verify_node( + network_middleware=network_middleware, + accept_federated_only=federated_only, + certificate_filepath=certificate_filepath) + + except potential_seed_node.InvalidNode: + raise # TODO: What if our seed node fails verification? + + # OK - everyone get out + temp_node_storage.forget() + return potential_seed_node + + @classmethod + def from_rest_url(cls, + network_middleware: RestMiddleware, + host: str, + port: int, + certificate_filepath, + federated_only: bool = False, + *args, **kwargs + ): + + response = network_middleware.node_information(host, port, certificate_filepath=certificate_filepath) + if not response.status_code == 200: + raise RuntimeError("Got a bad response: {}".format(response)) + + stranger_ursula_from_public_keys = cls.from_bytes(response.content, federated_only=federated_only, *args, + **kwargs) + return stranger_ursula_from_public_keys @classmethod def from_tls_hosting_power(cls, tls_hosting_power: TLSHostingPower, *args, **kwargs) -> 'Teacher': @@ -736,20 +853,19 @@ class Teacher: certificate = tls_hosting_power.keypair.certificate return cls(certificate=certificate, certificate_filepath=certificate_filepath, *args, **kwargs) + # + # Known Nodes + # + + def seed_node_metadata(self): + return SeednodeMetadata(self.checksum_public_address, # type: str + self.rest_server.rest_interface.host, # type: str + self.rest_server.rest_interface.port) # type: int + def sorted_nodes(self): nodes_to_consider = list(self.known_nodes.values()) + [self] return sorted(nodes_to_consider, key=lambda n: n.checksum_public_address) - def _stamp_has_valid_wallet_signature(self): - signature_bytes = self._evidence_of_decentralized_identity - if signature_bytes is constants.NOT_SIGNED: - return False - else: - signature = EthSignature(signature_bytes) - proper_pubkey = signature.recover_public_key_from_msg(bytes(self.stamp)) - proper_address = proper_pubkey.to_checksum_address() - return proper_address == self.checksum_public_address - def update_snapshot(self, checksum, updated): # TODO: Kind of an interesting pattern here - with VerifiableNode increasingly looking like it will be Teacher. # We update the simple snapshot here, but of course if we're dealing with an instance that is also a Learner, it has @@ -759,6 +875,19 @@ class Teacher: self.fleet_state_updated = updated self.fleet_state_icon = icon_from_checksum(self.fleet_state_checksum, nickname_metadata=self.fleet_state_nickname_metadata) + # + # Stamp + # + + def _stamp_has_valid_wallet_signature(self): + signature_bytes = self._evidence_of_decentralized_identity + if signature_bytes is constants.NOT_SIGNED: + return False + else: + signature = EthSignature(signature_bytes) + proper_pubkey = signature.recover_public_key_from_msg(bytes(self.stamp)) + proper_address = proper_pubkey.to_checksum_address() + return proper_address == self.checksum_public_address def stamp_is_valid(self): """ @@ -776,19 +905,6 @@ class Teacher: else: raise self.InvalidNode - def interface_is_valid(self): - """ - Checks that the interface info is valid for this node's canonical address. - """ - interface_info_message = self._signable_interface_info_message() # Contains canonical address. - message = self.timestamp_bytes() + interface_info_message - interface_is_valid = self._interface_signature.verify(message, self.public_keys(SigningPower)) - self.verified_interface = interface_is_valid - if interface_is_valid: - return True - else: - raise self.InvalidNode - def verify_id(self, ursula_id, digest_factory=bytes): self.verify() if not ursula_id == digest_factory(self.canonical_public_address): @@ -855,6 +971,23 @@ class Teacher: signature = blockchain_power.sign_message(bytes(self.stamp)) self._evidence_of_decentralized_identity = signature + # + # Interface + # + + def interface_is_valid(self): + """ + Checks that the interface info is valid for this node's canonical address. + """ + interface_info_message = self._signable_interface_info_message() # Contains canonical address. + message = self.timestamp_bytes() + interface_info_message + interface_is_valid = self._interface_signature.verify(message, self.public_keys(SigningPower)) + self.verified_interface = interface_is_valid + if interface_is_valid: + return True + else: + raise self.InvalidNode + def _signable_interface_info_message(self): message = self.canonical_public_address + self.rest_information()[0] return message @@ -885,102 +1018,9 @@ class Teacher: def timestamp_bytes(self): return self.timestamp.epoch.to_bytes(4, 'big') - @classmethod - def from_seednode_metadata(cls, - seednode_metadata, - node_storage=None, - *args, - **kwargs): - """ - Essentially another deserialization method, but this one doesn't reconstruct a complete - node from bytes; instead it's just enough to connect to and verify a node. - """ - - return cls.from_seed_and_stake_info(checksum_address=seednode_metadata.checksum_address, - host=seednode_metadata.rest_host, - port=seednode_metadata.rest_port, - node_storage=node_storage, - *args, **kwargs) - - @classmethod - @validate_checksum_address - def from_seed_and_stake_info(cls, - host, - port, - federated_only, - minimum_stake=__DEFAULT_MIN_SEED_STAKE, - checksum_address=None, - network_middleware=None, - *args, - **kwargs - ): - - # - # WARNING: xxx Poison xxx - # Let's learn what we can about the ... "seednode". - # - - if network_middleware is None: - network_middleware = RestMiddleware() - - # Fetch the hosts TLS certificate and read the common name - certificate = network_middleware.get_certificate(host=host, port=port) - real_host = certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value - temp_node_storage = ForgetfulNodeStorage(federated_only=federated_only) - certificate_filepath = temp_node_storage.store_host_certificate(host=real_host, - certificate=certificate) - # Load the host as a potential seed node - potential_seed_node = cls.from_rest_url( - host=real_host, - port=port, - network_middleware=network_middleware, - certificate_filepath=certificate_filepath, - federated_only=True, - *args, - **kwargs) # TODO: 466 - - if checksum_address: - # Ensure this is the specific node we expected - if not checksum_address == potential_seed_node.checksum_public_address: - raise potential_seed_node.SuspiciousActivity( - "This seed node has a different wallet address: {} (expected {}). Are you sure this is a seednode?" - .format(potential_seed_node.checksum_public_address, checksum_address)) - - # Check the node's stake (optional) - if minimum_stake > 0: - # TODO: check the blockchain to verify that address has more then minimum_stake. #511 - raise NotImplementedError("Stake checking is not implemented yet.") - - # Verify the node's TLS certificate - try: - potential_seed_node.verify_node( - network_middleware=network_middleware, - accept_federated_only=federated_only, - certificate_filepath=certificate_filepath) - - except potential_seed_node.InvalidNode: - raise # TODO: What if our seed node fails verification? - - # OK - everyone get out - temp_node_storage.forget() - return potential_seed_node - - @classmethod - def from_rest_url(cls, - network_middleware: RestMiddleware, - host: str, - port: int, - certificate_filepath, - federated_only: bool = False, - *args, **kwargs - ): - - response = network_middleware.node_information(host, port, certificate_filepath=certificate_filepath) - if not response.status_code == 200: - raise RuntimeError("Got a bad response: {}".format(response)) - - stranger_ursula_from_public_keys = cls.from_bytes(response.content, federated_only=federated_only, *args, **kwargs) - return stranger_ursula_from_public_keys + # + # Nicknames + # @property def nickname_icon(self): diff --git a/nucypher/network/protocols.py b/nucypher/network/protocols.py index de9953e54..4339989b3 100644 --- a/nucypher/network/protocols.py +++ b/nucypher/network/protocols.py @@ -14,6 +14,12 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with nucypher. If not, see . """ + + +from urllib.parse import urlparse + +from eth_utils import is_checksum_address + from bytestring_splitter import VariableLengthBytestring @@ -21,6 +27,26 @@ class SuspiciousActivity(RuntimeError): """raised when an action appears to amount to malicious conduct.""" +def parse_node_uri(uri: str): + from nucypher.config.characters import UrsulaConfiguration + + if '@' in uri: + checksum_address, uri = uri.split("@") + if not is_checksum_address(checksum_address): + raise ValueError("{} is not a valid checksum address.".format(checksum_address)) + else: + checksum_address = None # federated + + # HTTPS Explicit Required + parsed_uri = urlparse(uri) + if not parsed_uri.scheme == "https": + raise ValueError("Invalid teacher URI. Is the hostname prefixed with 'https://' ?") + + hostname = parsed_uri.hostname + port = parsed_uri.port or UrsulaConfiguration.DEFAULT_REST_PORT + return hostname, port, checksum_address + + class InterfaceInfo: expected_bytes_length = lambda: VariableLengthBytestring diff --git a/nucypher/network/server.py b/nucypher/network/server.py index 3a17169e4..e462e35fa 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -18,6 +18,8 @@ along with nucypher. If not, see . import binascii import os +from typing import Callable + from twisted.logger import Logger from apistar import Route, App