Neil Booth
8 years ago
8 changed files with 228 additions and 42 deletions
@ -0,0 +1,137 @@ |
|||
# 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 |
|||
from server.version import VERSION |
|||
|
|||
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) |
|||
version = 'X{}'.format(VERSION.split()[1]) |
|||
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): |
|||
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 |
|||
self.logger.info('new {}'.format(peer)) |
|||
except IndexError: |
|||
pass |
Loading…
Reference in new issue