You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
198 lines
6.9 KiB
198 lines
6.9 KiB
# Copyright (c) 2016, Neil Booth
|
|
#
|
|
# All rights reserved.
|
|
#
|
|
# See the file "LICENCE" for information about the copyright
|
|
# and warranty status of this software.
|
|
|
|
'''IRC connectivity to discover peers.
|
|
|
|
Only calling start() requires the IRC Python module.
|
|
'''
|
|
|
|
import asyncio
|
|
import re
|
|
import socket
|
|
|
|
from collections import namedtuple
|
|
|
|
from lib.hash import double_sha256
|
|
from lib.util import LoggedClass
|
|
|
|
|
|
class IRC(LoggedClass):
|
|
|
|
Peer = namedtuple('Peer', 'ip_addr host ports')
|
|
|
|
class DisconnectedError(Exception):
|
|
pass
|
|
|
|
def __init__(self, env):
|
|
super().__init__()
|
|
self.env = env
|
|
|
|
# If this isn't something a peer or client expects
|
|
# then you won't appear in the client's network dialog box
|
|
irc_address = (env.coin.IRC_SERVER, env.coin.IRC_PORT)
|
|
self.channel = env.coin.IRC_CHANNEL
|
|
self.prefix = env.coin.IRC_PREFIX
|
|
|
|
self.clients = []
|
|
self.nick = '{}{}'.format(self.prefix,
|
|
env.irc_nick if env.irc_nick else
|
|
double_sha256(env.report_host.encode())
|
|
[:5].hex())
|
|
self.clients.append( IrcClient(irc_address, self.nick,
|
|
env.report_host,
|
|
env.report_tcp_port,
|
|
env.report_ssl_port) )
|
|
if env.report_host_tor:
|
|
self.clients.append( IrcClient(irc_address, self.nick + '_tor',
|
|
env.report_host_tor,
|
|
env.report_tcp_port_tor,
|
|
env.report_ssl_port_tor) )
|
|
|
|
self.peer_regexp = re.compile('({}[^!]*)!'.format(self.prefix))
|
|
self.peers = {}
|
|
|
|
async def start(self, caught_up):
|
|
'''Start IRC connections once caught up if enabled in environment.'''
|
|
await caught_up.wait()
|
|
try:
|
|
if self.env.irc:
|
|
await self.join()
|
|
else:
|
|
self.logger.info('IRC is disabled')
|
|
except asyncio.CancelledError:
|
|
pass
|
|
except Exception as e:
|
|
self.logger.error(str(e))
|
|
|
|
async def join(self):
|
|
import irc.client as irc_client
|
|
from jaraco.stream import buffer
|
|
|
|
# see https://pypi.python.org/pypi/irc under DecodingInput
|
|
irc_client.ServerConnection.buffer_class = \
|
|
buffer.LenientDecodingLineBuffer
|
|
|
|
reactor = irc_client.Reactor()
|
|
for event in ['welcome', 'join', 'quit', 'kick', 'whoreply',
|
|
'namreply', 'disconnect']:
|
|
reactor.add_global_handler(event, getattr(self, 'on_' + event))
|
|
|
|
# Note: Multiple nicks in same channel will trigger duplicate events
|
|
for client in self.clients:
|
|
client.connection = reactor.server()
|
|
|
|
while True:
|
|
try:
|
|
for client in self.clients:
|
|
self.logger.info('Joining IRC in {} as "{}" with '
|
|
'real name "{}"'
|
|
.format(self.channel, client.nick,
|
|
client.realname))
|
|
client.connect()
|
|
while True:
|
|
reactor.process_once()
|
|
await asyncio.sleep(2)
|
|
except irc_client.ServerConnectionError as e:
|
|
self.logger.error('connection error: {}'.format(e))
|
|
except self.DisconnectedError:
|
|
self.logger.error('disconnected')
|
|
await asyncio.sleep(10)
|
|
|
|
def log_event(self, event):
|
|
self.logger.info('IRC event type {} source {} args {}'
|
|
.format(event.type, event.source, event.arguments))
|
|
|
|
def on_welcome(self, connection, event):
|
|
'''Called when we connect to irc server.'''
|
|
connection.join(self.channel)
|
|
|
|
def on_disconnect(self, connection, event):
|
|
'''Called if we are disconnected.'''
|
|
self.log_event(event)
|
|
raise self.DisconnectedError
|
|
|
|
def on_join(self, connection, event):
|
|
'''Called when someone new connects to our channel, including us.'''
|
|
match = self.peer_regexp.match(event.source)
|
|
if match:
|
|
connection.who(match.group(1))
|
|
|
|
def on_quit(self, connection, event):
|
|
'''Called when someone leaves our channel.'''
|
|
match = self.peer_regexp.match(event.source)
|
|
if match:
|
|
self.peers.pop(match.group(1), None)
|
|
|
|
def on_kick(self, connection, event):
|
|
'''Called when someone is kicked from our channel.'''
|
|
self.log_event(event)
|
|
match = self.peer_regexp.match(event.arguments[0])
|
|
if match:
|
|
self.peers.pop(match.group(1), None)
|
|
|
|
def on_namreply(self, connection, event):
|
|
'''Called repeatedly when we first connect to inform us of all users
|
|
in the channel.
|
|
|
|
The users are space-separated in the 2nd argument.
|
|
'''
|
|
for peer in event.arguments[2].split():
|
|
if peer.startswith(self.prefix):
|
|
connection.who(peer)
|
|
|
|
def on_whoreply(self, connection, event):
|
|
'''Called when a response to our who requests arrives.
|
|
|
|
The nick is the 4th argument, and real name is in the 6th
|
|
argument preceeded by '0 ' for some reason.
|
|
'''
|
|
try:
|
|
nick = event.arguments[4]
|
|
line = event.arguments[6].split()
|
|
try:
|
|
ip_addr = socket.gethostbyname(line[1])
|
|
except socket.error:
|
|
# Could be .onion or IPv6.
|
|
ip_addr = line[1]
|
|
peer = self.Peer(ip_addr, line[1], line[2:])
|
|
self.peers[nick] = peer
|
|
except (IndexError, UnicodeError):
|
|
# UnicodeError comes from invalid domains (issue #68)
|
|
pass
|
|
|
|
|
|
class IrcClient(LoggedClass):
|
|
|
|
VERSION = '1.0'
|
|
DEFAULT_PORTS = {'t': 50001, 's': 50002}
|
|
|
|
def __init__(self, irc_address, nick, host, tcp_port, ssl_port):
|
|
super().__init__()
|
|
self.irc_host, self.irc_port = irc_address
|
|
self.nick = nick
|
|
self.realname = self.create_realname(host, tcp_port, ssl_port)
|
|
self.connection = None
|
|
|
|
def connect(self, keepalive=60):
|
|
'''Connect this client to its IRC server'''
|
|
self.connection.connect(self.irc_host, self.irc_port, self.nick,
|
|
ircname=self.realname)
|
|
self.connection.set_keepalive(keepalive)
|
|
|
|
@classmethod
|
|
def create_realname(cls, host, tcp_port, ssl_port):
|
|
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', tcp_port)
|
|
ssl = port_text('s', ssl_port)
|
|
return '{} v{}{}{}'.format(host, cls.VERSION, tcp, ssl)
|
|
|