Browse Source

Convert PeerSession to use aiorpcX

patch-2
Neil Booth 7 years ago
parent
commit
e69b1d930f
  1. 370
      server/peers.py

370
server/peers.py

@ -17,7 +17,6 @@ from functools import partial
import aiorpcx import aiorpcx
from lib.jsonrpc import JSONSession
from lib.peer import Peer from lib.peer import Peer
import lib.util as util import lib.util as util
import server.version as version import server.version as version
@ -28,191 +27,178 @@ STALE_SECS = 24 * 3600
WAKEUP_SECS = 300 WAKEUP_SECS = 300
class PeerSession(JSONSession): class PeerSession(aiorpcx.ClientSession, util.LoggedClass):
'''An outgoing session to a peer.''' '''An outgoing session to a peer.'''
def __init__(self, peer, peer_mgr, kind): def __init__(self, peer, peer_mgr, kind, host, port, **kwargs):
super().__init__() super().__init__(host, port, **kwargs)
self.max_send = 0 util.LoggedClass.__init__(self)
self.peer = peer self.peer = peer
self.peer_mgr = peer_mgr self.peer_mgr = peer_mgr
self.kind = kind self.kind = kind
self.failed = False self.timeout = 20 if self.peer.is_tor else 10
self.bad = False
self.remote_peers = None
self.log_prefix = '[{}] '.format(self.peer)
async def wait_on_items(self):
while True:
await self.items_event.wait()
await self.process_pending_items()
def connection_made(self, transport): def connection_made(self, transport):
'''Handle an incoming client connection.''' '''Handle an incoming client connection.'''
super().connection_made(transport) super().connection_made(transport)
self.log_prefix = '[{}] '.format(str(self.peer)[:25])
self.future = self.peer_mgr.ensure_future(self.wait_on_items())
# Update IP address # Update IP address if not Tor
if not self.peer.is_tor: if not self.peer.is_tor:
peer_info = self.peer_info() address = self.peer_address()
if peer_info: if address:
self.peer.ip_addr = peer_info[0] self.peer.ip_addr = address[0]
# Collect data
proto_ver = (version.PROTOCOL_MIN, version.PROTOCOL_MAX)
self.send_request(self.on_version, 'server.version',
[version.VERSION, proto_ver])
self.send_request(self.on_features, 'server.features')
self.send_request(self.on_height, 'blockchain.headers.subscribe')
self.send_request(self.on_peers_subscribe, 'server.peers.subscribe')
def connection_lost(self, exc):
'''Handle disconnection.'''
super().connection_lost(exc)
self.future.cancel()
def on_peers_subscribe(self, result, error):
'''Handle the response to the peers.subcribe message.'''
if error:
self.failed = True
self.log_error('server.peers.subscribe: {}'.format(error))
else:
# Save for later analysis
self.remote_peers = result
self.close_if_done()
def on_add_peer(self, result, error):
'''We got a response the add_peer message.'''
# This is the last thing we were waiting for; shutdown the connection
self.shutdown_connection()
def on_features(self, features, error):
# Several peers don't implement this. If they do, check they are
# the same network with the genesis hash.
if not error and isinstance(features, dict):
hosts = [host.lower() for host in features.get('hosts', {})]
our_hash = self.peer_mgr.env.coin.GENESIS_HASH
if our_hash != features.get('genesis_hash'):
self.bad = True
self.log_warning('incorrect genesis hash')
elif self.peer.host.lower() in hosts:
self.peer.update_features(features)
else:
self.bad = True
self.log_warning('ignoring - not listed in host list {}'
.format(hosts))
self.close_if_done()
def on_height(self, result, error): # Send server.version first
'''Handle the response to blockchain.headers.subscribe message.''' args = [version.VERSION, [version.PROTOCOL_MIN, version.PROTOCOL_MAX]]
if error: self.send_request('server.version', args, self.on_version,
self.failed = True timeout=self.timeout)
self.log_error('blockchain.headers.subscribe returned an error')
elif not isinstance(result, dict): def is_good(self, request, instance):
self.bad = True try:
self.log_error('bad blockchain.headers.subscribe response') result = request.result()
else: except asyncio.CancelledError:
controller = self.peer_mgr.controller return False
our_height = controller.bp.db_height except asyncio.TimeoutError as e:
their_height = result.get('block_height') self.fail(request, str(e))
if not isinstance(their_height, int): return False
self.log_warning('invalid height {}'.format(their_height)) except RPCError as error:
self.bad = True self.fail(request, f'{error.message} ({error.code})')
elif abs(our_height - their_height) > 5: return False
self.log_warning('bad height {:,d} (ours: {:,d})'
.format(their_height, our_height))
self.bad = True
# Check prior header too in case of hard fork.
if not self.bad:
check_height = min(our_height, their_height)
self.send_request(self.on_header, 'blockchain.block.get_header',
[check_height])
self.expected_header = controller.electrum_header(check_height)
self.close_if_done()
def on_header(self, result, error):
'''Handle the response to blockchain.block.get_header message.
Compare hashes of prior header in attempt to determine if forked.'''
if error:
self.failed = True
self.log_error('blockchain.block.get_header returned an error')
elif not isinstance(result, dict):
self.bad = True
self.log_error('bad blockchain.block.get_header response')
else:
theirs = result.get('prev_block_hash')
ours = self.expected_header.get('prev_block_hash')
if ours != theirs:
self.log_error('our header hash {} and theirs {} differ'
.format(ours, theirs))
self.bad = True
self.close_if_done() if isinstance(result, instance):
return True
def on_version(self, result, error): self.fail(request, f'{request} returned bad result type '
f'{type(result).__name__}')
return False
def fail(self, request, reason):
self.logger.error(f'[{self.peer.host}] {request} failed: {reason}')
self.peer_mgr.set_verification_status(self.peer, self.kind, False)
self.close()
def bad(self, reason):
self.logger.error(f'[{self.peer.host}] marking bad: {reason}')
self.peer.mark_bad()
self.peer_mgr.set_verification_status(self.peer, self.kind, False)
self.close()
def on_version(self, request):
'''Handle the response to the version message.''' '''Handle the response to the version message.'''
if error: if not self.is_good(request, (list, str)):
self.failed = True return
self.log_error('server.version returned an error')
result = request.result()
if isinstance(result, str):
version = result
else: else:
# Protocol version 1.1 returns a pair with the version first # Protocol version 1.1 returns a pair with the version first
if isinstance(result, list) and len(result) == 2: if len(result) < 2 or not isinstance(result[0], str):
result = result[0] self.fail(request, 'result array bad format')
if isinstance(result, str): return
self.peer.server_version = result version = result[0]
self.peer.features['server_version'] = result self.peer.server_version = version
self.close_if_done() self.peer.features['server_version'] = version
for method, on_done in [
('blockchain.headers.subscribe', self.on_height),
('server.features', self.on_features),
('server.peers.subscribe', self.on_peers_subscribe),
]:
self.send_request(method, on_done=on_done, timeout=self.timeout)
def on_features(self, request):
if not self.is_good(request, dict):
return
def check_remote_peers(self): features = request.result()
'''Check the peers list we got from a remote peer. hosts = [host.lower() for host in features.get('hosts', {})]
our_hash = self.peer_mgr.env.coin.GENESIS_HASH
if our_hash != features.get('genesis_hash'):
self.bad('incorrect genesis hash')
elif self.peer.host.lower() in hosts:
self.peer.update_features(features)
self.maybe_close()
else:
self.bad('ignoring - not listed in host list {}'.format(hosts))
Each update is expected to be of the form: def on_height(self, request):
[ip_addr, hostname, ['v1.0', 't51001', 's51002']] '''Handle the response to blockchain.headers.subscribe message.'''
if not self.is_good(request, dict):
return
Call add_peer if the remote doesn't appear to know about us. result = request.result()
''' controller = self.peer_mgr.controller
try: our_height = controller.bp.db_height
real_names = [' '.join([u[1]] + u[2]) for u in self.remote_peers] their_height = result.get('block_height')
peers = [Peer.from_real_name(real_name, str(self.peer)) if not isinstance(their_height, int):
for real_name in real_names] self.bad('invalid height {}'.format(their_height))
except Exception: return
self.log_error('bad server.peers.subscribe response') if abs(our_height - their_height) > 5:
self.bad('bad height {:,d} (ours: {:,d})'
.format(their_height, our_height))
return
# Check prior header too in case of hard fork.
check_height = min(our_height, their_height)
expected_header = controller.electrum_header(check_height)
self.send_request('blockchain.block.get_header', [check_height],
partial(self.on_header, expected_header),
timeout=self.timeout)
def on_header(self, expected_header, request):
'''Handle the response to blockchain.block.get_header message.
Compare hashes of prior header in attempt to determine if forked.'''
if not self.is_good(request, dict):
return return
self.peer_mgr.add_peers(peers) result = request.result()
theirs = result.get('prev_block_hash')
ours = expected_header.get('prev_block_hash')
if ours == theirs:
self.maybe_close()
else:
self.bad('our header hash {} and theirs {} differ'
.format(ours, theirs))
# Announce ourself if not present. Don't if disabled, we def on_peers_subscribe(self, request):
# are a non-public IP address, or to ourselves. '''Handle the response to the peers.subcribe message.'''
if not self.peer_mgr.env.peer_announce: if not self.is_good(request, list):
return
if self.peer in self.peer_mgr.myselves:
return return
my = self.peer_mgr.my_clearnet_peer()
if not my or not my.is_public: # Check the peers list we got from a remote peer.
# Each is expected to be of the form:
# [ip_addr, hostname, ['v1.0', 't51001', 's51002']]
# Call add_peer if the remote doesn't appear to know about us.
raw_peers = request.result()
try:
real_names = [' '.join([u[1]] + u[2]) for u in raw_peers]
peers = [Peer.from_real_name(real_name, str(self.peer))
for real_name in real_names]
except Exception:
self.bad('bad server.peers.subscribe response')
return return
for peer in my.matches(peers):
if peer.tcp_port == my.tcp_port and peer.ssl_port == my.ssl_port:
return
self.log_info('registering ourself with server.add_peer') features = self.peer_mgr.features_to_register(self.peer, peers)
self.send_request(self.on_add_peer, 'server.add_peer', [my.features]) if features:
self.logger.info(f'[{self.peer.host}] registering ourself with '
'"server.add_peer"')
self.send_request('server.add_peer', [features],
self.on_add_peer, timeout=self.timeout)
else:
self.maybe_close()
def close_if_done(self): def on_add_peer(self, request):
if not self.has_pending_requests(): '''We got a response the add_peer message. Don't care about its
if self.bad: form.'''
self.peer.mark_bad() self.maybe_close()
elif self.remote_peers:
self.check_remote_peers()
# We might now be waiting for an add_peer response
if not self.has_pending_requests():
self.shutdown_connection()
def shutdown_connection(self): def maybe_close(self):
is_good = not (self.failed or self.bad) '''Close the connection if no requests are outstanding, and mark peer
self.peer_mgr.set_verification_status(self.peer, self.kind, is_good) as good.
self.close_connection() '''
if not self.all_requests():
self.close()
self.peer_mgr.set_verification_status(self.peer, self.kind, True)
class PeerManager(util.LoggedClass): class PeerManager(util.LoggedClass):
@ -287,6 +273,26 @@ class PeerManager(util.LoggedClass):
return [peer_data(peer) for peer in sorted(self.peers, key=peer_key)] return [peer_data(peer) for peer in sorted(self.peers, key=peer_key)]
def features_to_register(self, peer, remote_peers):
'''If we should register ourselves to the remote peer, which has
reported the given list of known peers, return the clearnet
identity features to register, otherwise None.
'''
self.add_peers(remote_peers)
# Announce ourself if not present. Don't if disabled, we
# are a non-public IP address, or to ourselves.
if not self.env.peer_announce or peer in self.myselves:
return None
my = self.my_clearnet_peer()
if not my or not my.is_public:
return None
# Register if no matches, or ports have changed
for peer in my.matches(remote_peers):
if peer.tcp_port == my.tcp_port and peer.ssl_port == my.ssl_port:
return None
return my.features
def add_peers(self, peers, limit=2, check_ports=False, source=None): def add_peers(self, peers, limit=2, check_ports=False, source=None):
'''Add a limited number of peers that are not already present.''' '''Add a limited number of peers that are not already present.'''
retry = False retry = False
@ -505,45 +511,43 @@ class PeerManager(util.LoggedClass):
def retry_peer(self, peer, port_pairs): def retry_peer(self, peer, port_pairs):
peer.last_try = time.time() peer.last_try = time.time()
kwargs = {'loop': self.loop}
kind, port = port_pairs[0] kind, port = port_pairs[0]
sslc = ssl.SSLContext(ssl.PROTOCOL_TLS) if kind == 'SSL' else None if kind == 'SSL':
kwargs['ssl'] = ssl.SSLContext(ssl.PROTOCOL_TLS)
host = self.env.cs_host(for_rpc=False) host = self.env.cs_host(for_rpc=False)
if isinstance(host, list): if isinstance(host, list):
host = host[0] host = host[0]
kwargs = {'ssl': sslc}
if self.env.force_proxy or peer.is_tor: if self.env.force_proxy or peer.is_tor:
# Only attempt a proxy connection if we have one
if not self.proxy: if not self.proxy:
return return
create_connection = self.proxy.create_connection kwargs['proxy'] = self.proxy
else: elif host:
create_connection = self.loop.create_connection # Use our listening Host/IP for outgoing non-proxy
# Use our listening Host/IP for outgoing connections so # connections so our peers see the correct source.
# our peers see the correct source. kwargs['local_addr'] = (host, None)
if host:
kwargs['local_addr'] = (host, None) session = PeerSession(peer, self, kind, peer.host, port, **kwargs)
callback = partial(self.on_connected, session, peer, port_pairs)
protocol_factory = partial(PeerSession, peer, self, kind) self.ensure_future(session.create_connection(), callback)
coro = create_connection(protocol_factory, peer.host, port, **kwargs)
callback = partial(self.connection_done, peer, port_pairs) def on_connected(self, session, peer, port_pairs, future):
self.ensure_future(coro, callback)
def connection_done(self, peer, port_pairs, future):
'''Called when a connection attempt succeeds or fails. '''Called when a connection attempt succeeds or fails.
If failed, log it and try remaining port pairs. If none, If failed, close the session, log it and try remaining port pairs.
release the connection count semaphore.
''' '''
exception = future.exception() exception = future.exception()
if exception: if exception:
kind, port = port_pairs[0] session.close()
self.logger.info('failed connecting to {} at {} port {:d} ' kind, port = port_pairs.pop(0)
'in {:.1f}s: {}' self.log_info('failed connecting to {} at {} port {:d} '
.format(peer, kind, port, 'in {:.1f}s: {}'
time.time() - peer.last_try, exception)) .format(peer, kind, port,
port_pairs = port_pairs[1:] time.time() - peer.last_try, exception))
if port_pairs: if port_pairs:
self.retry_peer(peer, port_pairs) self.retry_peer(peer, port_pairs)
else: else:

Loading…
Cancel
Save