asyncio: network/interface: remove broken code, use connecting/interfaces correctly

This commit is contained in:
Janus 2018-03-12 16:32:27 +01:00
parent b3bc3b4ad6
commit da3d10fd09
3 changed files with 108 additions and 140 deletions

View File

@ -108,58 +108,21 @@ class Interface(util.PrintError):
context = get_ssl_context(cert_reqs=ssl.CERT_REQUIRED, ca_certs=ca_path) context = get_ssl_context(cert_reqs=ssl.CERT_REQUIRED, ca_certs=ca_path)
else: else:
context = get_ssl_context(cert_reqs=ssl.CERT_NONE, ca_certs=None) context = get_ssl_context(cert_reqs=ssl.CERT_NONE, ca_certs=None)
try: if self.addr is not None:
if self.addr is not None: self.print_error("can't save certificate through socks!")
proto_factory = lambda: SSLProtocol(asyncio.get_event_loop(), asyncio.Protocol(), context, None) # just save the empty file to force use of PKI
socks_create_coro = aiosocks.create_connection(proto_factory, \ # this will break all self-signed servers, of course
proxy=self.addr, \
proxy_auth=self.auth, \
dst=(self.host, self.port))
transport, protocol = await asyncio.wait_for(socks_create_coro, 5)
async def job(fut):
try:
if protocol._sslpipe is not None:
fut.set_result(protocol._sslpipe.ssl_object.getpeercert(True))
except BaseException as e:
fut.set_exception(e)
while self.is_running():
fut = asyncio.Future()
asyncio.ensure_future(job(fut))
try:
await fut
except:
pass
try:
fut.exception()
dercert = fut.result()
except ValueError:
await asyncio.sleep(1)
continue
except:
if self.is_running():
traceback.print_exc()
print("Previous exception from _save_certificate")
continue
break
if not self.is_running(): return
transport.close()
else:
reader, writer = await asyncio.wait_for(self.conn_coro(context), 3)
dercert = writer.get_extra_info('ssl_object').getpeercert(True)
writer.close()
except OSError as e: # not ConnectionError because we need socket.gaierror too
if self.is_running():
self.print_error(self.server, "Exception in _save_certificate", type(e))
if not self.error_future.done(): self.error_future.set_result(e)
return
except TimeoutError:
return
assert dercert
if not require_ca:
cert = ssl.DER_cert_to_PEM_cert(dercert)
else:
# Don't pin a CA signed certificate
cert = "" cert = ""
else:
reader, writer = await asyncio.wait_for(self.conn_coro(context), 3)
dercert = writer.get_extra_info('ssl_object').getpeercert(True)
# an exception will be thrown by now if require_ca is True (e.g. a certificate was supplied)
writer.close()
if not require_ca:
cert = ssl.DER_cert_to_PEM_cert(dercert)
else:
# Don't pin a CA signed certificate
cert = ""
temporary_path = cert_path + '.temp' temporary_path = cert_path + '.temp'
with open(temporary_path, "w") as f: with open(temporary_path, "w") as f:
f.write(cert) f.write(cert)
@ -172,10 +135,26 @@ class Interface(util.PrintError):
if self.use_ssl: if self.use_ssl:
cert_path = os.path.join(self.config_path, 'certs', self.host) cert_path = os.path.join(self.config_path, 'certs', self.host)
if not os.path.exists(cert_path): if not os.path.exists(cert_path):
temporary_path = await self._save_certificate(cert_path, True) temporary_path = None
# first, we try to save a certificate signed through the PKI
try:
temporary_path = await self._save_certificate(cert_path, True)
except ssl.SSLError:
pass
except (TimeoutError, OSError) as e:
if not self.error_future.done(): self.error_future.set_result(e)
raise
# if the certificate verification failed, we try to save a self-signed certificate
if not temporary_path: if not temporary_path:
try:
temporary_path = await self._save_certificate(cert_path, False) temporary_path = await self._save_certificate(cert_path, False)
# we also catch SSLError here, but it shouldn't matter since no certificate is required,
# so the SSLError wouldn't mean certificate validation failed
except (TimeoutError, OSError) as e:
if not self.error_future.done(): self.error_future.set_result(e)
raise
if not temporary_path: if not temporary_path:
if not self.error_future.done(): self.error_future.set_result(ConnectionError("Could not get certificate"))
raise ConnectionError("Could not get certificate on second try") raise ConnectionError("Could not get certificate on second try")
is_new = True is_new = True
@ -201,6 +180,10 @@ class Interface(util.PrintError):
except TimeoutError: except TimeoutError:
self.print_error("TimeoutError after getting certificate successfully...") self.print_error("TimeoutError after getting certificate successfully...")
raise raise
except ssl.SSLError:
# FIXME TODO
assert not self_signed, "we shouldn't reject self-signed here since the certificate has been saved (has size {})".format(size)
raise
except BaseException as e: except BaseException as e:
if self.is_running(): if self.is_running():
if not isinstance(e, OSError): if not isinstance(e, OSError):

View File

@ -167,8 +167,12 @@ class Network(util.DaemonThread):
""" """
def __init__(self, config=None): def __init__(self, config=None):
self.lock = threading.Lock()
# callbacks set by the GUI
self.callbacks = defaultdict(list)
self.set_status("disconnected")
self.disconnected_servers = {} self.disconnected_servers = {}
self.connecting = set() self.connecting = {}
self.stopped = True self.stopped = True
asyncio.set_event_loop(None) asyncio.set_event_loop(None)
if config is None: if config is None:
@ -192,7 +196,6 @@ class Network(util.DaemonThread):
self.default_server = None self.default_server = None
if not self.default_server: if not self.default_server:
self.default_server = pick_random_server() self.default_server = pick_random_server()
self.lock = threading.Lock()
self.message_id = 0 self.message_id = 0
self.debug = False self.debug = False
self.irc_servers = {} # returned by interface (list from irc) self.irc_servers = {} # returned by interface (list from irc)
@ -204,8 +207,6 @@ class Network(util.DaemonThread):
# callbacks passed with subscriptions # callbacks passed with subscriptions
self.subscriptions = defaultdict(list) self.subscriptions = defaultdict(list)
self.sub_cache = {} self.sub_cache = {}
# callbacks set by the GUI
self.callbacks = defaultdict(list)
dir_path = os.path.join( self.config.path, 'certs') dir_path = os.path.join( self.config.path, 'certs')
if not os.path.exists(dir_path): if not os.path.exists(dir_path):
@ -221,7 +222,7 @@ class Network(util.DaemonThread):
self.server_retry_time = time.time() self.server_retry_time = time.time()
self.nodes_retry_time = time.time() self.nodes_retry_time = time.time()
# kick off the network. interface is the main server we are currently # kick off the network. interface is the main server we are currently
# communicating with. interfaces is the set of servers we are connecting # communicating with. interfaces is the set of servers we are connected
# to or have an ongoing connection with # to or have an ongoing connection with
self.interface = None self.interface = None
self.interfaces = {} self.interfaces = {}
@ -385,18 +386,26 @@ class Network(util.DaemonThread):
return out return out
async def start_interface(self, server): async def start_interface(self, server):
if server not in self.interfaces and server not in self.connecting: if server not in self.interfaces and server not in self.connecting and not self.stopped:
self.connecting.add(server) self.connecting[server] = asyncio.Future()
if server == self.default_server: if server == self.default_server:
self.print_error("connecting to %s as new interface" % server) self.print_error("connecting to %s as new interface" % server)
self.set_status('connecting') self.set_status('connecting')
return self.new_interface(server) iface = self.new_interface(server)
success = await iface.launched
# if the interface was launched sucessfully, we save it in interfaces
# since that dictionary only stores "good" interfaces
if success:
assert server not in self.interfaces
self.interfaces[server] = iface
if not self.connecting[server].done(): self.connecting[server].set_result(True)
del self.connecting[server]
async def start_random_interface(self): async def start_random_interface(self):
exclude_set = set(self.disconnected_servers.keys()).union(set(self.interfaces.keys())) exclude_set = set(self.disconnected_servers.keys()).union(set(self.interfaces.keys()))
server = pick_random_server(self.get_servers(), self.protocol, exclude_set) server = pick_random_server(self.get_servers(), self.protocol, exclude_set)
if server: if server:
return await self.start_interface(server) await self.start_interface(server)
def start_network(self, protocol, proxy): def start_network(self, protocol, proxy):
self.stopped = False self.stopped = False
@ -411,8 +420,14 @@ class Network(util.DaemonThread):
self.print_error("stopping network") self.print_error("stopping network")
async def stop(interface): async def stop(interface):
await self.connection_down(interface.server, "stopping network") await self.connection_down(interface.server, "stopping network")
await interface.future await interface.boot_job
stopped_this_time = set() stopped_this_time = set()
while self.connecting:
try:
await next(iter(self.connecting.values()))
except asyncio.CancelledError:
# ok since we are shutting down
pass
while self.interfaces: while self.interfaces:
do_next = next(iter(self.interfaces.values())) do_next = next(iter(self.interfaces.values()))
stopped_this_time.add(do_next) stopped_this_time.add(do_next)
@ -507,15 +522,14 @@ class Network(util.DaemonThread):
async def close_interface(self, interface): async def close_interface(self, interface):
self.print_error('closing connection', interface.server) self.print_error('closing connection', interface.server)
if interface: if interface.server in self.interfaces:
if interface.server in self.interfaces: self.interfaces.pop(interface.server)
self.interfaces.pop(interface.server) if interface.server == self.default_server:
if interface.server == self.default_server: self.interface = None
self.interface = None assert not interface.boot_job.cancelled()
if interface.jobs: interface.boot_job.cancel()
await asyncio.gather(*interface.jobs) await interface.boot_job
await interface.boot_job interface.close()
interface.close()
def add_recent_server(self, server): def add_recent_server(self, server):
# list is ordered # list is ordered
@ -708,18 +722,14 @@ class Network(util.DaemonThread):
'''A connection to server either went down, or was never made. '''A connection to server either went down, or was never made.
We distinguish by whether it is in self.interfaces.''' We distinguish by whether it is in self.interfaces.'''
if server in self.disconnected_servers: if server in self.disconnected_servers:
try: print("already disconnected " + server + " because " + repr(self.disconnected_servers[server]) + ". new reason: " + repr(reason))
raise Exception("already disconnected " + server + " because " + repr(self.disconnected_servers[server]) + ". new reason: " + repr(reason))
except:
traceback.print_exc()
return return
self.print_error("connection down", server, reason) self.print_error("connection down", server, reason)
for i in self.interfaces[server].jobs:
assert not i.cancelled()
i.cancel()
self.disconnected_servers[server] = reason self.disconnected_servers[server] = reason
if server == self.default_server: if server == self.default_server:
self.set_status('disconnected') self.set_status('disconnected')
if server in self.connecting:
await self.connecting[server]
if server in self.interfaces: if server in self.interfaces:
await self.close_interface(self.interfaces[server]) await self.close_interface(self.interfaces[server])
self.notify('interfaces') self.notify('interfaces')
@ -733,11 +743,12 @@ class Network(util.DaemonThread):
def interface_exception_handler(exception_future): def interface_exception_handler(exception_future):
try: try:
raise exception_future.result() raise exception_future.result()
except Exception as e: except BaseException as e:
print(type(e), " handled in new_interface") asyncio.ensure_future(self.connection_down(server, "error in interface: " + str(e)))
asyncio.ensure_future(self.connection_down(server, "error in interface")) interface = Interface(server, self.config.path, self.proxy, lambda: not self.stopped and (server in self.connecting or server in self.interfaces), interface_exception_handler)
interface = Interface(server, self.config.path, self.proxy, lambda: not self.stopped and server in self.interfaces, interface_exception_handler) # this future has its result set when the interface should be considered opened,
interface.future = asyncio.Future() # e.g. should be moved from connecting to interfaces
interface.launched = asyncio.Future()
interface.blockchain = None interface.blockchain = None
interface.tip_header = None interface.tip_header = None
interface.tip = 0 interface.tip = 0
@ -748,7 +759,6 @@ class Network(util.DaemonThread):
self.boot_interface(interface) self.boot_interface(interface)
assert server not in self.interfaces assert server not in self.interfaces
assert not self.stopped assert not self.stopped
self.interfaces[server] = interface
return interface return interface
async def request_chunk(self, interface, idx): async def request_chunk(self, interface, idx):
@ -916,15 +926,17 @@ class Network(util.DaemonThread):
async def job(): async def job():
try: try:
while True: while True:
await interface.send_request() # this wait_for is necessary since CancelledError
# doesn't seem to get thrown without it
# when using ssl_in_socks
try:
await asyncio.wait_for(interface.send_request(), 1)
except TimeoutError:
pass
except CancelledError: except CancelledError:
pass pass
except Exception as e: except BaseException as e:
print("send requests exp", str(e)) await self.connection_down(interface.server, "exp while send_request: " + str(e))
if interface.is_running() or self.num_server == 0:
traceback.print_exc()
self.print_error("FATAL ERROR ^^^")
await self.connection_down(interface.server, "exp while send_request")
return asyncio.ensure_future(job()) return asyncio.ensure_future(job())
@ -932,15 +944,10 @@ class Network(util.DaemonThread):
async def job(): async def job():
try: try:
await self.process_responses(interface) await self.process_responses(interface)
except OSError:
await self.connection_down(interface.server, "OSError in process_responses")
self.print_error("OS error, connection downed")
except CancelledError: except CancelledError:
pass pass
except: except:
if interface.is_running() or self.num_server == 0: await self.connection_down(interface.server, "exp in process_responses")
traceback.print_exc()
self.print_error("FATAL ERROR in process_responses")
return asyncio.ensure_future(job()) return asyncio.ensure_future(job())
def make_process_pending_sends_job(self): def make_process_pending_sends_job(self):
@ -951,9 +958,7 @@ class Network(util.DaemonThread):
except CancelledError: except CancelledError:
pass pass
except: except:
if not self.stopped or self.num_server == 0: await self.connection_down(interface.server, "exp in process_pending_sends_job")
traceback.print_exc()
self.print_error("FATAL ERROR in process_pending_sends")
return asyncio.ensure_future(job()) return asyncio.ensure_future(job())
def init_headers_file(self): def init_headers_file(self):
@ -972,30 +977,17 @@ class Network(util.DaemonThread):
async def job(): async def job():
try: try:
interface.jobs = [self.make_send_requests_job(interface)] # we need this job to process the request queued below interface.jobs = [self.make_send_requests_job(interface)] # we need this job to process the request queued below
try: await asyncio.wait_for(self.queue_request('server.version', [ELECTRUM_VERSION, PROTOCOL_VERSION], interface), 10)
await asyncio.wait_for(self.queue_request('server.version', [ELECTRUM_VERSION, PROTOCOL_VERSION], interface), 1) handshakeResult = await asyncio.wait_for(interface.get_response(), 10)
except TimeoutError:
asyncio.ensure_future(self.connection_down(interface.server, "couldn't send initial version"))
interface.future.set_result("couldn't send initial version")
return
if not interface.is_running(): if not interface.is_running():
interface.future.set_result("stopped after sending request") self.print_error("WARNING: quitting bootjob instead of handling CancelledError!")
return return
try:
await asyncio.wait_for(interface.get_response(), 1)
except TimeoutError:
asyncio.ensure_future(self.connection_down(interface.server, "timeout in boot_interface while getting response"))
interface.future.set_result("timeout while getting response")
return
if not interface.is_running():
interface.future.set_result("stopped after getting response")
return
#self.interfaces[interface.server] = interface
await self.queue_request('blockchain.headers.subscribe', [], interface) await self.queue_request('blockchain.headers.subscribe', [], interface)
if interface.server == self.default_server: if interface.server == self.default_server:
await asyncio.wait_for(self.switch_to_interface(interface.server), 1) await asyncio.wait_for(self.switch_to_interface(interface.server), 5)
interface.jobs += [self.make_process_responses_job(interface)] interface.jobs += [self.make_process_responses_job(interface)]
gathered = asyncio.gather(*interface.jobs) gathered = asyncio.gather(*interface.jobs)
interface.launched.set_result(handshakeResult)
while interface.is_running(): while interface.is_running():
try: try:
await asyncio.wait_for(asyncio.shield(gathered), 1) await asyncio.wait_for(asyncio.shield(gathered), 1)
@ -1008,26 +1000,18 @@ class Network(util.DaemonThread):
elif interface.ping_required(): elif interface.ping_required():
params = [ELECTRUM_VERSION, PROTOCOL_VERSION] params = [ELECTRUM_VERSION, PROTOCOL_VERSION]
await self.queue_request('server.version', params, interface) await self.queue_request('server.version', params, interface)
interface.future.set_result("finished")
return
#self.notify('interfaces') #self.notify('interfaces')
except BaseException as e: except TimeoutError:
if interface.is_running() or self.num_server == 0: asyncio.ensure_future(self.connection_down(interface.server, "timeout in boot_interface while getting response"))
traceback.print_exc() except CancelledError:
self.print_error("FATAL ERROR in boot_interface") pass
raise e
finally:
self.connecting.remove(interface.server)
interface.boot_job = asyncio.ensure_future(job())
interface.boot_job.server = interface.server
def boot_job_cb(fut):
try:
fut.exception()
except: except:
traceback.print_exc() traceback.print_exc()
self.print_error("Previous exception in boot_job") finally:
interface.boot_job.add_done_callback(boot_job_cb) for i in interface.jobs: i.cancel()
if not interface.launched.done(): interface.launched.set_result(None)
interface.boot_job = asyncio.ensure_future(job())
interface.boot_job.server = interface.server
async def maintain_interfaces(self): async def maintain_interfaces(self):
if self.stopped: if self.stopped:
@ -1035,7 +1019,8 @@ class Network(util.DaemonThread):
now = time.time() now = time.time()
# nodes # nodes
if len(self.interfaces) + len(self.connecting) < self.num_server: if len(self.interfaces) + len(self.connecting) < self.num_server:
await self.start_random_interface() # ensure future so that servers can be connected to in parallel
asyncio.ensure_future(self.start_random_interface())
if now - self.nodes_retry_time > NODES_RETRY_INTERVAL: if now - self.nodes_retry_time > NODES_RETRY_INTERVAL:
self.print_error('network: retrying connections') self.print_error('network: retrying connections')
self.disconnected_servers = {} self.disconnected_servers = {}
@ -1072,10 +1057,10 @@ class Network(util.DaemonThread):
asyncio.ensure_future(self.run_async(run_future)) asyncio.ensure_future(self.run_async(run_future))
loop.run_until_complete(run_future) loop.run_until_complete(run_future)
assert self.forever_coroutines_task.done() loop.run_until_complete(self.forever_coroutines_task)
run_future.exception() run_future.exception()
self.print_error("run future result", run_future.result()) # we don't want to wait for the closure of all connections, the OS can do that
loop.close() #loop.close()
async def run_async(self, future): async def run_async(self, future):
try: try:

View File

@ -228,6 +228,7 @@ class DaemonThread(threading.Thread, PrintError):
self.forever_coroutines_queue = asyncio.Queue() # making queue here because __init__ is called from non-network thread self.forever_coroutines_queue = asyncio.Queue() # making queue here because __init__ is called from non-network thread
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
async def getFromQueueAndStart(): async def getFromQueueAndStart():
jobs = []
while True: while True:
try: try:
jobs = await asyncio.wait_for(self.forever_coroutines_queue.get(), 1) jobs = await asyncio.wait_for(self.forever_coroutines_queue.get(), 1)
@ -236,7 +237,6 @@ class DaemonThread(threading.Thread, PrintError):
if not self.is_running(): break if not self.is_running(): break
continue continue
await asyncio.gather(*[i.run(self.is_running) for i in jobs]) await asyncio.gather(*[i.run(self.is_running) for i in jobs])
self.print_error("FOREVER JOBS DONE")
self.forever_coroutines_task = asyncio.ensure_future(getFromQueueAndStart()) self.forever_coroutines_task = asyncio.ensure_future(getFromQueueAndStart())
return self.forever_coroutines_task return self.forever_coroutines_task