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.
146 lines
4.8 KiB
146 lines
4.8 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
|
|
|
|
|
|
def port_text(letter, port, default):
|
|
if not port:
|
|
return ''
|
|
if port == default:
|
|
return letter
|
|
return letter + str(port)
|
|
|
|
|
|
class IRC(LoggedClass):
|
|
|
|
PEER_REGEXP = re.compile('(E_[^!]*)!')
|
|
Peer = namedtuple('Peer', 'ip_addr host ports')
|
|
|
|
class DisconnectedError(Exception):
|
|
pass
|
|
|
|
def __init__(self, env):
|
|
super().__init__()
|
|
tcp_text = port_text('t', env.report_tcp_port, 50001)
|
|
ssl_text = port_text('s', env.report_ssl_port, 50002)
|
|
# If this isn't something the client expects you won't appear
|
|
# in the client's network dialog box
|
|
version = '1.0'
|
|
self.real_name = '{} v{} {} {}'.format(env.report_host, version,
|
|
tcp_text, ssl_text)
|
|
self.nick = 'E_{}'.format(env.irc_nick if env.irc_nick else
|
|
double_sha256(env.report_host.encode())
|
|
[:5].hex())
|
|
self.peers = {}
|
|
|
|
async def start(self):
|
|
try:
|
|
await self.join()
|
|
except asyncio.CancelledError:
|
|
pass
|
|
except Exception as e:
|
|
self.logger.error(str(e))
|
|
|
|
async def join(self):
|
|
import irc.client as irc_client
|
|
|
|
self.logger.info('joining IRC with nick "{}" and real name "{}"'
|
|
.format(self.nick, self.real_name))
|
|
|
|
reactor = irc_client.Reactor()
|
|
for event in ['welcome', 'join', 'quit', 'kick', 'whoreply',
|
|
'namreply', 'disconnect']:
|
|
reactor.add_global_handler(event, getattr(self, 'on_' + event))
|
|
|
|
while True:
|
|
try:
|
|
connection = reactor.server()
|
|
connection.connect('irc.freenode.net', 6667,
|
|
self.nick, ircname=self.real_name)
|
|
connection.set_keepalive(60)
|
|
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 freenode.'''
|
|
connection.join('#electrum')
|
|
|
|
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("E_"):
|
|
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:
|
|
# No IPv4 address could be resolved. Could be .onion or IPv6.
|
|
ip_addr = line[1]
|
|
peer = self.Peer(ip_addr, line[1], line[2:])
|
|
self.peers[nick] = peer
|
|
except IndexError:
|
|
pass
|
|
|