5 changed files with 186 additions and 112 deletions
@ -0,0 +1,139 @@ |
|||||
|
# Copyright (c) 2017, Neil Booth |
||||
|
# |
||||
|
# All rights reserved. |
||||
|
# |
||||
|
# See the file "LICENCE" for information about the copyright |
||||
|
# and warranty status of this software. |
||||
|
|
||||
|
'''Peer management.''' |
||||
|
|
||||
|
import asyncio |
||||
|
import socket |
||||
|
import traceback |
||||
|
from collections import namedtuple |
||||
|
from functools import partial |
||||
|
|
||||
|
import lib.util as util |
||||
|
from server.irc import IRC |
||||
|
|
||||
|
|
||||
|
NetIdentity = namedtuple('NetIdentity', 'host tcp_port ssl_port nick_suffix') |
||||
|
IRCPeer = namedtuple('IRCPeer', 'ip_addr host details') |
||||
|
|
||||
|
|
||||
|
class PeerManager(util.LoggedClass): |
||||
|
'''Looks after the DB of peer network servers. |
||||
|
|
||||
|
Attempts to maintain a connection with up to 8 peers. |
||||
|
Issues a 'peers.subscribe' RPC to them and tells them our data. |
||||
|
''' |
||||
|
VERSION = '1.0' |
||||
|
DEFAULT_PORTS = {'t': 50001, 's': 50002} |
||||
|
|
||||
|
def __init__(self, env): |
||||
|
super().__init__() |
||||
|
self.env = env |
||||
|
self.loop = asyncio.get_event_loop() |
||||
|
self.irc = IRC(env, self) |
||||
|
self.futures = set() |
||||
|
self.identities = [] |
||||
|
# Keyed by nick |
||||
|
self.irc_peers = {} |
||||
|
|
||||
|
# We can have a Tor identity inaddition to a normal one |
||||
|
self.identities.append(NetIdentity(env.report_host, |
||||
|
env.report_tcp_port, |
||||
|
env.report_ssl_port, |
||||
|
'')) |
||||
|
if env.report_host_tor.endswith('.onion'): |
||||
|
self.identities.append(NetIdentity(env.report_host_tor, |
||||
|
env.report_tcp_port_tor, |
||||
|
env.report_ssl_port_tor, |
||||
|
'_tor')) |
||||
|
|
||||
|
async def executor(self, func, *args, **kwargs): |
||||
|
'''Run func taking args in the executor.''' |
||||
|
await self.loop.run_in_executor(None, partial(func, *args, **kwargs)) |
||||
|
|
||||
|
@classmethod |
||||
|
def real_name(cls, identity): |
||||
|
'''Real name as used on IRC.''' |
||||
|
def port_text(letter, port): |
||||
|
if not port: |
||||
|
return '' |
||||
|
if port == cls.DEFAULT_PORTS.get(letter): |
||||
|
return ' ' + letter |
||||
|
else: |
||||
|
return ' ' + letter + str(port) |
||||
|
|
||||
|
tcp = port_text('t', identity.tcp_port) |
||||
|
ssl = port_text('s', identity.ssl_port) |
||||
|
return '{} v{}{}{}'.format(identity.host, cls.VERSION, tcp, ssl) |
||||
|
|
||||
|
def ensure_future(self, coro): |
||||
|
'''Convert a coro into a future and add it to our pending list |
||||
|
to be waited for.''' |
||||
|
self.futures.add(asyncio.ensure_future(coro)) |
||||
|
|
||||
|
def start_irc(self): |
||||
|
'''Start up the IRC connections if enabled.''' |
||||
|
if self.env.irc: |
||||
|
name_pairs = [(self.real_name(identity), identity.nick_suffix) |
||||
|
for identity in self.identities] |
||||
|
self.ensure_future(self.irc.start(name_pairs)) |
||||
|
else: |
||||
|
self.logger.info('IRC is disabled') |
||||
|
|
||||
|
async def main_loop(self): |
||||
|
'''Start and then enter the main loop.''' |
||||
|
self.start_irc() |
||||
|
|
||||
|
try: |
||||
|
while True: |
||||
|
await asyncio.sleep(10) |
||||
|
done = [future for future in self.futures if future.done()] |
||||
|
self.futures.difference_update(done) |
||||
|
for future in done: |
||||
|
try: |
||||
|
future.result() |
||||
|
except: |
||||
|
self.log_error(traceback.format_exc()) |
||||
|
finally: |
||||
|
for future in self.futures: |
||||
|
future.cancel() |
||||
|
|
||||
|
def dns_lookup_peer(self, nick, hostname, details): |
||||
|
try: |
||||
|
ip_addr = None |
||||
|
try: |
||||
|
ip_addr = socket.gethostbyname(hostname) |
||||
|
except socket.error: |
||||
|
pass # IPv6? |
||||
|
ip_addr = ip_addr or hostname |
||||
|
self.irc_peers[nick] = IRCPeer(ip_addr, hostname, details) |
||||
|
self.logger.info('new IRC peer {} at {} ({})' |
||||
|
.format(nick, hostname, details)) |
||||
|
except UnicodeError: |
||||
|
# UnicodeError comes from invalid domains (issue #68) |
||||
|
self.logger.info('IRC peer domain {} invalid'.format(hostname)) |
||||
|
|
||||
|
def add_irc_peer(self, *args): |
||||
|
'''Schedule DNS lookup of peer.''' |
||||
|
self.ensure_future(self.executor(self.dns_lookup_peer, *args)) |
||||
|
|
||||
|
def remove_irc_peer(self, nick): |
||||
|
'''Remove a peer from our IRC peers map.''' |
||||
|
self.logger.info('removing IRC peer {}'.format(nick)) |
||||
|
self.irc_peers.pop(nick, None) |
||||
|
|
||||
|
def count(self): |
||||
|
return len(self.irc_peers) |
||||
|
|
||||
|
def peer_list(self): |
||||
|
return self.irc_peers |
||||
|
|
||||
|
async def subscribe(self): |
||||
|
'''Returns the server peers as a list of (ip, host, details) tuples. |
||||
|
|
||||
|
Despite the name this is not currently treated as a subscription.''' |
||||
|
return list(self.irc_peers.values()) |
Loading…
Reference in new issue