Organize Learner / Server methods; Recompose after diffusing cli utilities.

pull/562/head
Kieran Prasch 2018-11-22 10:28:33 -08:00
parent 14ba280294
commit 6cbe8e5f3d
No known key found for this signature in database
GPG Key ID: 199AB839D4125A62
3 changed files with 191 additions and 123 deletions

View File

@ -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):

View File

@ -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 <https://www.gnu.org/licenses/>.
"""
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

View File

@ -18,6 +18,8 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import binascii
import os
from typing import Callable
from twisted.logger import Logger
from apistar import Route, App