From d23d1c3987363c1e87883840e41a213396486521 Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 5 Aug 2015 21:56:52 -0700 Subject: [PATCH 01/27] relay: don't wait for irc.connected anymoree Not needed, as nick length is a config value now. --- plugins/relay.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 7b65f3f..896917b 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -30,13 +30,6 @@ def relayWhoisHandlers(irc, target): utils.whois_handlers.append(relayWhoisHandlers) def normalizeNick(irc, netname, nick, separator=None, oldnick=''): - # Block until we know the IRC network's nick length (after capabilities - # are sent) - if not hasattr(irc, 'relay_waitFinished'): - log.debug('(%s) normalizeNick: waiting for irc.connected', irc.name) - irc.connected.wait(1) - irc.relay_waitFinished = True - separator = separator or irc.serverdata.get('separator') or "/" log.debug('(%s) normalizeNick: using %r as separator.', irc.name, separator) @@ -49,7 +42,7 @@ def normalizeNick(irc, netname, nick, separator=None, oldnick=''): separator = separator.replace('/', '|') nick = nick.replace('/', '|') if nick.startswith(tuple(string.digits)): - # On TS6 IRCd-s, nicks that start with 0-9 are only allowed if + # On TS6 IRCds, nicks that start with 0-9 are only allowed if # they match the UID of the originating server. Otherwise, you'll # get nasty protocol violations! nick = '_' + nick @@ -134,7 +127,7 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): u = relayusers[(irc.name, user)][remoteirc.name] except KeyError: userobj = irc.users.get(user) - if userobj is None or (not spawnIfMissing) or (not remoteirc.connected): + if userobj is None or (not spawnIfMissing) or (not remoteirc.connected.is_set()): # The query wasn't actually a valid user, or the network hasn't # been connected yet... Oh well! return @@ -241,7 +234,7 @@ def initializeChannel(irc, channel): if remoteirc is None: continue rc = remoteirc.channels[remotechan] - if not (remoteirc.connected and findRemoteChan(remoteirc, irc, remotechan)): + if not (remoteirc.connected.is_set() and findRemoteChan(remoteirc, irc, remotechan)): continue # They aren't connected, don't bother! # Join their (remote) users and set their modes. relayJoins(remoteirc, remotechan, rc.users, From cdb0bb670721a6dc60d50ea3a6975e2275570b78 Mon Sep 17 00:00:00 2001 From: James Lu Date: Fri, 7 Aug 2015 17:57:37 -0700 Subject: [PATCH 02/27] relay: fix some dumb typos --- plugins/relay.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 896917b..35c2996 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -401,8 +401,8 @@ def handle_kick(irc, source, command, args): # Join the kicked client back with its respective modes. irc.proto.sjoinServer(irc, irc.sid, remotechan, [(modes, target)]) if kicker in irc.users: - utils.msg(irc, kicker, "This channel is claimed; your kick has " - "to %s been blocked because you are not " + utils.msg(irc, kicker, "This channel is claimed; your kick to " + "%s has been blocked because you are not " "(half)opped." % channel, notice=True) return @@ -626,7 +626,7 @@ def handle_kill(irc, numeric, command, args): client = getRemoteUser(remoteirc, irc, realuser[1]) irc.proto.sjoinServer(irc, irc.sid, remotechan, [(modes, client)]) if userdata and numeric in irc.users: - utils.msg(irc, numeric, "Your kill has to %s been blocked " + utils.msg(irc, numeric, "Your kill to %s has been blocked " "because PyLink does not allow killing" " users over the relay at this time." % \ userdata.nick, notice=True) From b6f5b08af0e170b9b12f139a0fb23aace73d8dbf Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 9 Aug 2015 14:45:15 +0800 Subject: [PATCH 03/27] README: add elemental-ircd to supported IRCds --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 55599ff..b149bf8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ PyLink is an extensible, plugin-based IRC PseudoService written in Python. It ai ## Usage -**PyLink is a work in progress and thus may be very unstable**! No warranty is provided if this completely wrecks your network and causes widespread rioting throughout your user base! +**PyLink is a work in progress and thus may be very unstable**! No warranty is provided if this completely wrecks your network and causes widespread rioting amongst your users! That said, please report any bugs you find to the [issue tracker](https://github.com/GLolol/PyLink/issues). Pull requests are open if you'd like to contribute. @@ -19,6 +19,7 @@ Dependencies currently include: * InspIRCd 2.0.x - module: `inspircd` * charybdis (3.5.x / git master) - module: `ts6` +* Elemental-IRCd (6.6.x / git master) - module `ts6` ### Installation From 18cd3bdd88bc388a2e0151a190830635c6221927 Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 10 Aug 2015 20:24:55 -0700 Subject: [PATCH 04/27] Add SSL linking support (#80) TODO: implement fingerprint checking (optional) and a genssl script to ease SSL certificate generation. --- config.yml.example | 6 ++++++ main.py | 19 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/config.yml.example b/config.yml.example index fc4453d..1f6be96 100644 --- a/config.yml.example +++ b/config.yml.example @@ -53,6 +53,12 @@ servers: # PyLink might introduce a nick that is too long and cause netsplits! maxnicklen: 30 + # Toggles SSL for this network. Defaults to false if not specified, and requires the + # ssl_certfile and ssl_keyfile options to work. + # ssl: true + # ssl_certfile: pylink-cert.pem + # ssl_keyfile: pylink-key.pem + ts6net: ip: 127.0.0.1 port: 7000 diff --git a/main.py b/main.py index 3bb3b3e..7d1af4b 100755 --- a/main.py +++ b/main.py @@ -7,6 +7,7 @@ import time import sys from collections import defaultdict import threading +import ssl from log import log import conf @@ -73,13 +74,25 @@ class Irc(): ip = self.serverdata["ip"] port = self.serverdata["port"] while True: - log.info("Connecting to network %r on %s:%s", self.name, ip, port) self.initVars() try: + self.socket = socket.socket() + self.socket.setblocking(0) # Initial connection timeout is a lot smaller than the timeout after # we've connected; this is intentional. - self.socket = socket.create_connection((ip, port), timeout=self.pingfreq) - self.socket.setblocking(0) + self.socket.settimeout(self.pingfreq) + + if self.serverdata.get('ssl'): + log.info('(%s) Attempting SSL for this connection...', self.name) + certfile = self.serverdata.get('ssl_certfile') + keyfile = self.serverdata.get('ssl_keyfile') + if certfile and keyfile: + self.socket = ssl.wrap_socket(self.socket, certfile=certfile, keyfile=keyfile) + else: + log.warning('(%s) SSL certfile/keyfile was not set correctly. ' + 'SSL will be disabled for this connection.', self.name) + log.info("Connecting to network %r on %s:%s", self.name, ip, port) + self.socket.connect((ip, port)) self.socket.settimeout(self.pingtimeout) self.proto.connect(self) self.spawnMain() From e756af42eed8437632ec1d086e93079e6d352acd Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 10 Aug 2015 20:26:10 -0700 Subject: [PATCH 05/27] .gitignore: ignore .pem files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d1e6f0b..fb899c8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__/ *.save* *.db *.pid +*.pem From 684eac95832aa8ff15490a6fc4396ba61cf013ef Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 10 Aug 2015 20:26:19 -0700 Subject: [PATCH 06/27] Irc: fix the logic checking for data in run() --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 7d1af4b..2c6336f 100755 --- a/main.py +++ b/main.py @@ -130,8 +130,8 @@ class Irc(): log.debug('(%s) self.pingtimeout: %s', self.name, self.pingtimeout) data = self.socket.recv(2048) buf += data - if self.connected and not data: - log.warn('(%s) No data received and self.connected is not set; disconnecting!', self.name) + if self.connected.is_set() and not data: + log.warn('(%s) No data received and self.connected is set; disconnecting!', self.name) break while b'\n' in buf: line, buf = buf.split(b'\n', 1) From 14435f8f55c9c3eadb17ccac59c3f432799350bb Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 01:19:30 -0700 Subject: [PATCH 07/27] relay: run getLocalUser BEFORE quitting the user (in removeChannel) This prevents KeyErrors from showing up on DELINK. --- plugins/relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index 35c2996..d1fc9b5 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -718,9 +718,9 @@ def removeChannel(irc, channel): irc.proto.partClient(irc, user, channel, 'Channel delinked.') # Don't ever quit it either... if user != irc.pseudoclient.uid and not irc.users[user].channels: - irc.proto.quitClient(irc, user, 'Left all shared channels.') remoteuser = getLocalUser(irc, user) del relayusers[remoteuser][irc.name] + irc.proto.quitClient(irc, user, 'Left all shared channels.') @utils.add_cmd def create(irc, source, args): From b3c8929d17e5ad39d4ab14ea027021cf93b52bee Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 01:20:32 -0700 Subject: [PATCH 08/27] relay: make logging less spammy --- plugins/relay.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index d1fc9b5..69f09a2 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -109,7 +109,8 @@ def getPrefixModes(irc, remoteirc, channel, user): for pmode in ('owner', 'admin', 'op', 'halfop', 'voice'): if pmode in remoteirc.cmodes: # Mode supported by IRCd mlist = irc.channels[channel].prefixmodes[pmode+'s'] - log.debug('(%s) getPrefixModes: checking if %r is in %r', irc.name, user, mlist) + log.debug('(%s) getPrefixModes: checking if %r is in %s list: %r', + irc.name, user, pmode, mlist) if user in mlist: modes += remoteirc.cmodes[pmode] return modes @@ -158,11 +159,9 @@ def getLocalUser(irc, user, targetirc=None): # First, iterate over everyone! remoteuser = None for k, v in relayusers.items(): - log.debug('(%s) getLocalUser: processing %s, %s in relayusers', irc.name, k, v) if k[0] == irc.name: # We don't need to do anything if the target users is on # the same network as us. - log.debug('(%s) getLocalUser: skipping %s since the target network matches the source network.', irc.name, k) continue if v.get(irc.name) == user: # If the stored pseudoclient UID for the kicked user on From fe7adb716bf33bc0eed33438381af97cc23be9bc Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 01:20:44 -0700 Subject: [PATCH 09/27] start-cpulimit: throttle at 35% instead of killing at 20% usage --- start-cpulimit.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/start-cpulimit.sh b/start-cpulimit.sh index 5c2a582..308df68 100755 --- a/start-cpulimit.sh +++ b/start-cpulimit.sh @@ -1,18 +1,17 @@ #!/usr/bin/env bash -# Shell script to start PyLink under CPUlimit, killing it if it starts abusing the CPU. +# Shell script to start PyLink under CPUlimit, throttling it if it starts abusing the CPU. # Set this to whatever you want. cpulimit --help -LIMIT=20 +LIMIT=35 # Change to the PyLink root directory. WRAPPER_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) cd "$WRAPPER_DIR" if [[ ! -z "$(which cpulimit)" ]]; then - # -k kills the PyLink daemon if it goes over $LIMIT # -z makes cpulimit exit when PyLink dies. - cpulimit -l $LIMIT -z -k ./main.py - echo "PyLink has been started (daemonized) under cpulimit, and will automatically be killed if it goes over the CPU limit of ${LIMIT}%." + cpulimit -l $LIMIT -z ./main.py + echo "PyLink has been started (daemonized) under cpulimit, and will automatically be throttled if it goes over the CPU limit of ${LIMIT}%." echo "To kill the process manually, run ./kill.sh" else echo 'cpulimit not found in $PATH! Aborting.' From d9f5cdfeafac18fb738c0561b1df1d3f58efb416 Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 02:00:43 -0700 Subject: [PATCH 10/27] Irc: optionally validate SSL cert fingerprints (#80) --- config.yml.example | 5 +++++ main.py | 52 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/config.yml.example b/config.yml.example index 1f6be96..c091cce 100644 --- a/config.yml.example +++ b/config.yml.example @@ -56,9 +56,14 @@ servers: # Toggles SSL for this network. Defaults to false if not specified, and requires the # ssl_certfile and ssl_keyfile options to work. # ssl: true + # ssl_certfile: pylink-cert.pem # ssl_keyfile: pylink-key.pem + # Optionally, you can set this option to verify the SSL certificate + # fingerprint (SHA1) of your uplink. + # ssl_fingerprint: "e0fee1adf795c84eec4735f039503eb18d9c35cc" + ts6net: ip: 127.0.0.1 port: 7000 diff --git a/main.py b/main.py index 2c6336f..a1ef7d5 100755 --- a/main.py +++ b/main.py @@ -8,6 +8,7 @@ import sys from collections import defaultdict import threading import ssl +import hashlib from log import log import conf @@ -75,31 +76,62 @@ class Irc(): port = self.serverdata["port"] while True: self.initVars() + checks_ok = True try: self.socket = socket.socket() self.socket.setblocking(0) # Initial connection timeout is a lot smaller than the timeout after # we've connected; this is intentional. self.socket.settimeout(self.pingfreq) - - if self.serverdata.get('ssl'): + self.ssl = self.serverdata.get('ssl') + if self.ssl: log.info('(%s) Attempting SSL for this connection...', self.name) certfile = self.serverdata.get('ssl_certfile') keyfile = self.serverdata.get('ssl_keyfile') if certfile and keyfile: self.socket = ssl.wrap_socket(self.socket, certfile=certfile, keyfile=keyfile) else: - log.warning('(%s) SSL certfile/keyfile was not set correctly. ' - 'SSL will be disabled for this connection.', self.name) + log.error('(%s) SSL certfile/keyfile was not set ' + 'correctly, aborting... ', self.name) + checks_ok = False log.info("Connecting to network %r on %s:%s", self.name, ip, port) self.socket.connect((ip, port)) self.socket.settimeout(self.pingtimeout) - self.proto.connect(self) - self.spawnMain() - log.info('(%s) Starting ping schedulers....', self.name) - self.schedulePing() - log.info('(%s) Server ready; listening for data.', self.name) - self.run() + + if self.ssl and checks_ok: + peercert = self.socket.getpeercert(binary_form=True) + sha1fp = hashlib.sha1(peercert).hexdigest() + expected_fp = self.serverdata.get('ssl_fingerprint') + if expected_fp: + if sha1fp != expected_fp: + log.error('(%s) Uplink\'s SSL certificate ' + 'fingerprint (SHA1) does not match the ' + 'one configured: expected %r, got %r; ' + 'disconnecting...', self.name, + expected_fp, sha1fp) + checks_ok = False + else: + log.info('(%s) Uplink SSL certificate fingerprint ' + '(SHA1) verified: %r', self.name, sha1fp) + else: + log.info('(%s) Uplink\'s SSL certificate fingerprint ' + 'is %r. You can enhance the security of your ' + 'link by specifying this in a "ssl_fingerprint"' + ' option in your server block.', self.name, + sha1fp) + + if checks_ok: + self.proto.connect(self) + self.spawnMain() + log.info('(%s) Starting ping schedulers....', self.name) + self.schedulePing() + log.info('(%s) Server ready; listening for data.', self.name) + self.run() + else: + log.error('(%s) A configuration error was encountered ' + 'trying to set up this connection. Please check' + ' your configuration file and try again.', + self.name) except (socket.error, classes.ProtocolError, ConnectionError) as e: log.warning('(%s) Disconnected from IRC: %s: %s', self.name, type(e).__name__, str(e)) From 7bccb37ddf69d8ded5af255b6835cff7977a7fdf Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 02:01:17 -0700 Subject: [PATCH 11/27] Irc: catch OSError when reading SSL cert/key files (ref: #80) --- main.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index a1ef7d5..d377592 100755 --- a/main.py +++ b/main.py @@ -89,7 +89,17 @@ class Irc(): certfile = self.serverdata.get('ssl_certfile') keyfile = self.serverdata.get('ssl_keyfile') if certfile and keyfile: - self.socket = ssl.wrap_socket(self.socket, certfile=certfile, keyfile=keyfile) + try: + self.socket = ssl.wrap_socket(self.socket, + certfile=certfile, + keyfile=keyfile) + except OSError: + log.exception('(%s) Caught OSError trying to ' + 'initialize the SSL connection; ' + 'are "ssl_certfile" and ' + '"ssl_keyfile" set correctly?', + self.name) + checks_ok = False else: log.error('(%s) SSL certfile/keyfile was not set ' 'correctly, aborting... ', self.name) From 1a57dfcdc3915e39867597e0ace9d57a06b2bc2c Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 04:17:01 -0700 Subject: [PATCH 12/27] prs+IrcUser: add incoming (handle_away) and outgoing (awayClient) AWAY handling --- classes.py | 1 + protocols/inspircd.py | 20 ++++++++++++++++++++ protocols/ts6.py | 20 ++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/classes.py b/classes.py index 729250f..0b7b8dc 100644 --- a/classes.py +++ b/classes.py @@ -21,6 +21,7 @@ class IrcUser(): self.identified = False self.channels = set() + self.away = '' def __repr__(self): return repr(self.__dict__) diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 995dc6c..253516c 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -297,6 +297,16 @@ def numericServer(irc, source, numeric, text): "locally by InspIRCd servers, so there is no " "need for PyLink to send numerics directly yet.") +def awayClient(irc, source, text): + """ + + Sends an AWAY message with text from PyLink client . + can be an empty string to unset AWAY status.""" + if text: + _send(irc, source, 'AWAY %s :%s' % (int(time.time()), text)) + else: + _send(irc, source, 'AWAY') + def connect(irc): ts = irc.start_ts @@ -664,3 +674,13 @@ def handle_fname(irc, numeric, command, args): def handle_endburst(irc, numeric, command, args): return {} + +def handle_away(irc, numeric, command, args): + # <- :1MLAAAAIG AWAY 1439371390 :Auto-away + try: + ts = args[0] + irc.users[numeric].away = text = args[1] + return {'text': text, 'ts': ts} + except IndexError: # User is unsetting away status + irc.users[numeric].away = '' + return {'text': ''} diff --git a/protocols/ts6.py b/protocols/ts6.py index 6a4f4e5..7f555e5 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -231,6 +231,16 @@ def pingServer(irc, source=None, target=None): def numericServer(irc, source, numeric, target, text): _send(irc, source, '%s %s %s' % (numeric, target, text)) +def awayClient(irc, source, text): + """ + + Sends an AWAY message with text from PyLink client . + can be an empty string to unset AWAY status.""" + if text: + _send(irc, source, 'AWAY :%s' % text) + else: + _send(irc, source, 'AWAY') + def connect(irc): ts = irc.start_ts @@ -649,3 +659,13 @@ def handle_472(irc, numeric, command, args): ' desyncs, try adding the line "loadmodule "extensions/%s.so";" to ' 'your IRCd configuration.', irc.name, setter, badmode, charlist[badmode]) + +def handle_away(irc, numeric, command, args): + # <- :6ELAAAAAB AWAY :Auto-away + + try: + irc.users[numeric].away = text = args[0] + except IndexError: # User is unsetting away status + irc.users[numeric].away = text = '' + return {'text': text} + From 486b56e255cf36438a3ef60a4fb0bbd62f2527fb Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 04:18:20 -0700 Subject: [PATCH 13/27] relay: relay AWAY statuses --- plugins/relay.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/relay.py b/plugins/relay.py index 69f09a2..3e29a7d 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -932,3 +932,9 @@ def linked(irc, source, args): else: s += '(no relays yet)' utils.msg(irc, source, s) + +def handle_away(irc, numeric, command, args): + for netname, user in relayusers[(irc.name, numeric)].items(): + remoteirc = utils.networkobjects[netname] + remoteirc.proto.awayClient(remoteirc, user, args['text']) +utils.add_hook(handle_away, 'AWAY') From 3ee10a5d1ed9c5a9037ffd6345840f9d3c95d4cb Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 04:40:49 -0700 Subject: [PATCH 14/27] relay: also propagate AWAY status when spawning users --- plugins/relay.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/relay.py b/plugins/relay.py index 3e29a7d..00343cc 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -143,6 +143,9 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): host=host, realname=realname, modes=modes, ts=userobj.ts).uid remoteirc.users[u].remote = irc.name + away = userobj.away + if away: + remoteirc.proto.awayClient(remoteirc, u, away) relayusers[(irc.name, user)][remoteirc.name] = u return u From 27edc81894978bfbb435198e451a8cc95b5a897e Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 07:03:49 -0700 Subject: [PATCH 15/27] relay: fix error message when DELINK is called on a relay's origin network without arguments tl;dr I'm a terrible writer --- plugins/relay.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index 00343cc..b8b2c5d 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -849,7 +849,10 @@ def delink(irc, source, args): if entry: if entry[0] == irc.name: # We own this channel. if not remotenet: - utils.msg(irc, source, "Error: you must select a network to delink, or use the 'destroy' command no remove this relay entirely.") + utils.msg(irc, source, "Error: You must select a network to " + "delink, or use the 'destroy' command to remove " + "this relay entirely (it was created on the current " + "network).") return else: for link in db[entry]['links'].copy(): From 7d912bbb2854a8d705088ee7650eaac1d181eb42 Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 07:05:05 -0700 Subject: [PATCH 16/27] pr/insp: Strip out listmodes in FJOIN They should always be sent separately according to the protocol documentation: https://wiki.inspircd.org/InspIRCd_Spanning_Tree_1.2/FJOIN Closes #58. --- protocols/inspircd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 253516c..7ac1f3c 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -80,7 +80,8 @@ def sjoinServer(irc, server, channel, users, ts=None): else: utils.applyModes(irc, channel, modes) ''' - modes = irc.channels[channel].modes + # Strip out list-modes, they shouldn't be ever sent in FJOIN. + modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] uids = [] changedmodes = [] namelist = [] From 88c156b8fc265fd2bc1bd78b210c042ec75ceff0 Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 22:17:44 +0800 Subject: [PATCH 17/27] README: formatting --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b149bf8..33d4788 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ Dependencies currently include: #### Supported IRCds -* InspIRCd 2.0.x - module: `inspircd` -* charybdis (3.5.x / git master) - module: `ts6` +* InspIRCd 2.0.x - module `inspircd` +* charybdis (3.5.x / git master) - module `ts6` * Elemental-IRCd (6.6.x / git master) - module `ts6` ### Installation From 18576ad4881a9f52e6bb0c612e959b1e97fc6d64 Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 12 Aug 2015 21:31:46 -0700 Subject: [PATCH 18/27] Irc: fix the ping timeout/reconnection logic - reset connection state and last ping time on reconnect - move Ping timeout logic to somewhere where it'd actually run - the code for breaking out of the run() loop on a ping timeout could previously only run when a ping timeout hadn't occured yet - consistently use log.warning() instead of log.warn() This hopefully fixes PyLink going into a disconnect/reconnect loop sometimes instead of reconnecting properly. --- main.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index d377592..bdf34e3 100755 --- a/main.py +++ b/main.py @@ -20,6 +20,8 @@ class Irc(): def initVars(self): # Server, channel, and user indexes to be populated by our protocol module + self.connected = threading.Event() + self.lastping = time.time() self.servers = {self.sid: classes.IrcServer(None, self.serverdata['hostname'], internal=True)} self.users = {} self.channels = defaultdict(classes.IrcChannel) @@ -54,7 +56,6 @@ class Irc(): def __init__(self, netname, proto, conf): # Initialize some variables - self.connected = threading.Event() self.name = netname.lower() self.conf = conf self.serverdata = conf['servers'][netname] @@ -69,7 +70,6 @@ class Irc(): self.connection_thread = threading.Thread(target = self.connect) self.connection_thread.start() self.pingTimer = None - self.lastping = time.time() def connect(self): ip = self.serverdata["ip"] @@ -167,14 +167,15 @@ class Irc(): def run(self): buf = b"" data = b"" - while (time.time() - self.lastping) < self.pingtimeout: - log.debug('(%s) time_since_last_ping: %s', self.name, (time.time() - self.lastping)) - log.debug('(%s) self.pingtimeout: %s', self.name, self.pingtimeout) + while True: data = self.socket.recv(2048) buf += data if self.connected.is_set() and not data: - log.warn('(%s) No data received and self.connected is set; disconnecting!', self.name) - break + log.warning('(%s) No data received and self.connected is set; disconnecting!', self.name) + return + elif (time.time() - self.lastping) > self.pingtimeout: + log.warning('(%s) Connection timed out.', self.name) + return while b'\n' in buf: line, buf = buf.split(b'\n', 1) line = line.strip(b'\r') From 13e4baba8bcd51d66b3ea8c2649c97c8578c337a Mon Sep 17 00:00:00 2001 From: James Lu Date: Fri, 14 Aug 2015 08:47:23 -0700 Subject: [PATCH 19/27] inspircd: also strip listmodes in joinClient (#58) --- protocols/inspircd.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 7ac1f3c..a783e09 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -56,10 +56,11 @@ def joinClient(irc, client, channel): if not server: log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', irc.name, client, channel) raise LookupError('No such PyLink PseudoClient exists.') - # One channel per line here! + # Strip out list-modes, they shouldn't be ever sent in FJOIN. + modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] _send(irc, server, "FJOIN {channel} {ts} {modes} :,{uid}".format( ts=irc.channels[channel].ts, uid=client, channel=channel, - modes=utils.joinModes(irc.channels[channel].modes))) + modes=utils.joinModes(modes))) irc.channels[channel].users.add(client) irc.users[client].channels.add(channel) @@ -74,12 +75,6 @@ def sjoinServer(irc, server, channel, users, ts=None): ts = irc.channels[channel].ts log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, irc.name, ts, time.strftime("%c", time.localtime(ts))) - ''' TODO: handle this properly! - if modes is None: - modes = irc.channels[channel].modes - else: - utils.applyModes(irc, channel, modes) - ''' # Strip out list-modes, they shouldn't be ever sent in FJOIN. modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] uids = [] From 65b8c9db8aba8e3b1bafd5ef65a4941aca7e57ae Mon Sep 17 00:00:00 2001 From: James Lu Date: Fri, 14 Aug 2015 08:52:09 -0700 Subject: [PATCH 20/27] relay: attempt to fix race conditions in getRemoteUser calls (#92) --- plugins/relay.py | 51 +++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index b8b2c5d..afdc0d3 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -16,7 +16,9 @@ dbname = "pylinkrelay" if confname != 'pylink': dbname += '-%s' % confname dbname += '.db' + relayusers = defaultdict(dict) +spawnlocks = defaultdict(threading.Lock) def relayWhoisHandlers(irc, target): user = irc.users[target] @@ -124,30 +126,31 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): return remoteirc.pseudoclient.uid except AttributeError: # Network hasn't been initialized yet? pass - try: - u = relayusers[(irc.name, user)][remoteirc.name] - except KeyError: - userobj = irc.users.get(user) - if userobj is None or (not spawnIfMissing) or (not remoteirc.connected.is_set()): - # The query wasn't actually a valid user, or the network hasn't - # been connected yet... Oh well! - return - nick = normalizeNick(remoteirc, irc.name, userobj.nick) - # Truncate idents at 10 characters, because TS6 won't like them otherwise! - ident = userobj.ident[:10] - # Ditto hostname at 64 chars. - host = userobj.host[:64] - realname = userobj.realname - modes = getSupportedUmodes(irc, remoteirc, userobj.modes) - u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident, - host=host, realname=realname, - modes=modes, ts=userobj.ts).uid - remoteirc.users[u].remote = irc.name - away = userobj.away - if away: - remoteirc.proto.awayClient(remoteirc, u, away) - relayusers[(irc.name, user)][remoteirc.name] = u - return u + with spawnlocks[irc.name]: + try: + u = relayusers[(irc.name, user)][remoteirc.name] + except KeyError: + userobj = irc.users.get(user) + if userobj is None or (not spawnIfMissing) or (not remoteirc.connected.is_set()): + # The query wasn't actually a valid user, or the network hasn't + # been connected yet... Oh well! + return + nick = normalizeNick(remoteirc, irc.name, userobj.nick) + # Truncate idents at 10 characters, because TS6 won't like them otherwise! + ident = userobj.ident[:10] + # Ditto hostname at 64 chars. + host = userobj.host[:64] + realname = userobj.realname + modes = getSupportedUmodes(irc, remoteirc, userobj.modes) + u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident, + host=host, realname=realname, + modes=modes, ts=userobj.ts).uid + remoteirc.users[u].remote = irc.name + away = userobj.away + if away: + remoteirc.proto.awayClient(remoteirc, u, away) + relayusers[(irc.name, user)][remoteirc.name] = u + return u def getLocalUser(irc, user, targetirc=None): """ [] From 13b53771020b6a5e2277c94792d0cdf3053ed3db Mon Sep 17 00:00:00 2001 From: James Lu Date: Fri, 14 Aug 2015 19:02:51 -0700 Subject: [PATCH 21/27] relay: Fix check for whether target user is on our target channel Really closes #71; can I sleep now... --- plugins/relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index afdc0d3..f4777de 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -675,7 +675,7 @@ def relayJoins(irc, channel, users, ts, modes): u = getRemoteUser(irc, remoteirc, user) # Only join users if they aren't already joined. This prevents op floods # on charybdis from all the SJOINing. - if u not in remoteirc.channels[channel].users: + if u not in remoteirc.channels[remotechan].users: ts = irc.channels[channel].ts prefixes = getPrefixModes(irc, remoteirc, channel, user) userpair = (prefixes, u) From e5c7d438b1253e416a53c5aebec8579e854a3161 Mon Sep 17 00:00:00 2001 From: James Lu Date: Fri, 14 Aug 2015 19:05:07 -0700 Subject: [PATCH 22/27] relay: in handle_kick, only remove the target from the user cache after the kick has been relayed to all networks --- plugins/relay.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index f4777de..130af99 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -389,7 +389,7 @@ def handle_kick(irc, source, command, args): # they originate from the same network. We won't have # to filter this; the uplink IRCd will handle it appropriately, # and we'll just follow. - real_target = getRemoteUser(irc, remoteirc, target) + real_target = getRemoteUser(irc, remoteirc, target, spawnIfMissing=False) log.debug('(%s) Relay kick: real target for %s is %s', irc.name, target, real_target) else: log.debug('(%s) Relay kick: target %s is an internal client, going to look up the real user', irc.name, target) @@ -431,10 +431,10 @@ def handle_kick(irc, source, command, args): remoteirc.proto.kickServer(remoteirc, remoteirc.sid, remotechan, real_target, text) - if target != irc.pseudoclient.uid and not irc.users[target].channels: - irc.proto.quitClient(irc, target, 'Left all shared channels.') - remoteuser = getLocalUser(irc, target) - del relayusers[remoteuser][irc.name] + if target != irc.pseudoclient.uid and not irc.users[target].channels: + irc.proto.quitClient(irc, target, 'Left all shared channels.') + remoteuser = getLocalUser(irc, target) + del relayusers[remoteuser][irc.name] utils.add_hook(handle_kick, 'KICK') From 4382b2213822dba84f68270d9427823fbcf60e80 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 15 Aug 2015 00:02:46 -0700 Subject: [PATCH 23/27] relay: warn users when messaging channels they aren't in / remote users w/o sharing a channel Some refactoring of relay's PRIVMSG handling is done here: - Messages to channels the sender isn't in are dropped, with a notice warning sent. - Messaging a remote user without sharing a channel with them raises an error, and is dropped. Closes #91. --- plugins/relay.py | 70 ++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 130af99..5788cc9 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -311,57 +311,51 @@ def handle_privmsg(irc, numeric, command, args): text = args['text'] if target == irc.pseudoclient.uid: return - sent = 0 relay = findRelay((irc.name, target)) - # Don't send any "you must be in common channels" if we're not part - # of a relay, or we are but there are no links! - remoteusers = relayusers[(irc.name, numeric)].items() - ''' - if utils.isChannel(target) and ((relay and not db[relay]['links']) or \ - relay is None): + remoteusers = relayusers[(irc.name, numeric)] + if utils.isChannel(target) and relay and numeric not in irc.channels[target].users: + # The sender must be in the target channel to send messages over the relay; + # it's the only way we can make sure they have a spawned client on ALL + # of the linked networks. This affects -n channels too; see + # https://github.com/GLolol/PyLink/issues/91 for an explanation of why. + utils.msg(irc, numeric, 'Error: You must be in %r in order to send ' + 'messages over the relay.' % target, notice=True) return - ''' - if not remoteusers: - return - for netname, user in relayusers[(irc.name, numeric)].items(): - remoteirc = utils.networkobjects[netname] - # HACK: Don't break on sending to @#channel or similar. - try: + if utils.isChannel(target): + for netname, user in relayusers[(irc.name, numeric)].items(): + remoteirc = utils.networkobjects[netname] + # HACK: Don't break on sending to @#channel or similar. prefix, target = target.split('#', 1) - except ValueError: - prefix = '' - else: target = '#' + target - if utils.isChannel(target): log.debug('(%s) relay privmsg: prefix is %r, target is %r', irc.name, prefix, target) real_target = findRemoteChan(irc, remoteirc, target) if not real_target: continue real_target = prefix + real_target - else: - remoteuser = getLocalUser(irc, target) - if remoteuser is None: - continue - real_target = remoteuser[1] + if notice: + remoteirc.proto.noticeClient(remoteirc, user, real_target, text) + else: + remoteirc.proto.messageClient(remoteirc, user, real_target, text) + else: + remoteuser = getLocalUser(irc, target) + if remoteuser is None: + return + homenet, real_target = remoteuser + # For PMs, we must be on a common channel with the target. + # Otherwise, the sender doesn't have a client representing them + # on the remote network, and we won't have anything to send our + # messages from. + if homenet not in remoteusers.keys(): + utils.msg(irc, numeric, 'Error: you must be in a common channel ' + 'with %r in order to send messages.' % \ + irc.users[target].nick, notice=True) + return + remoteirc = utils.networkobjects[homenet] + user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False) if notice: remoteirc.proto.noticeClient(remoteirc, user, real_target, text) else: remoteirc.proto.messageClient(remoteirc, user, real_target, text) - sent += 1 - ''' - if not sent: - # We must be on a common channel with the target. Otherwise, the sender - # doesn't have a client representing them on the remote network, - # and we won't have anywhere to send our messages from. - # In this case, we've iterated over all networks where the sender - # has pseudoclients, and found no suitable targets to send to. - if target in irc.users: - target_s = 'a common channel with %r' % irc.users[target].nick - else: - target_s = repr(target) - utils.msg(irc, numeric, 'Error: You must be in %s in order to send messages.' % \ - target_s, notice=True) - ''' utils.add_hook(handle_privmsg, 'PRIVMSG') utils.add_hook(handle_privmsg, 'NOTICE') From 5daf38d8807eb30488b66ce960678843c3f7ee25 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 15 Aug 2015 00:11:48 -0700 Subject: [PATCH 24/27] relay: restore ability to message @#channels across the relay (#91) --- plugins/relay.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 5788cc9..39356fc 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -313,6 +313,14 @@ def handle_privmsg(irc, numeric, command, args): return relay = findRelay((irc.name, target)) remoteusers = relayusers[(irc.name, numeric)] + # HACK: Don't break on sending to @#channel or similar. + try: + prefix, target = target.split('#', 1) + except ValueError: + prefix = '' + else: + target = '#' + target + log.debug('(%s) relay privmsg: prefix is %r, target is %r', irc.name, prefix, target) if utils.isChannel(target) and relay and numeric not in irc.channels[target].users: # The sender must be in the target channel to send messages over the relay; # it's the only way we can make sure they have a spawned client on ALL @@ -324,10 +332,6 @@ def handle_privmsg(irc, numeric, command, args): if utils.isChannel(target): for netname, user in relayusers[(irc.name, numeric)].items(): remoteirc = utils.networkobjects[netname] - # HACK: Don't break on sending to @#channel or similar. - prefix, target = target.split('#', 1) - target = '#' + target - log.debug('(%s) relay privmsg: prefix is %r, target is %r', irc.name, prefix, target) real_target = findRemoteChan(irc, remoteirc, target) if not real_target: continue From f53e818438c2285b54bf7c455d67d491ed79254c Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 15 Aug 2015 04:51:32 -0700 Subject: [PATCH 25/27] plugins/admin: add 'msg' command (#90) --- plugins/admin.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/plugins/admin.py b/plugins/admin.py index 892d4f3..1cbfdf3 100644 --- a/plugins/admin.py +++ b/plugins/admin.py @@ -208,3 +208,30 @@ def mode(irc, source, args): else: sourceuid = utils.nickToUid(irc, modesource) irc.proto.modeClient(irc, sourceuid, target, parsedmodes) + +@utils.add_cmd +def msg(irc, source, args): + """ + + Admin-only. Sends message from , where is the nick of a PyLink client.""" + checkauthenticated(irc, source) + try: + msgsource, target, text = args[0], args[1], ' '.join(args[2:]) + except IndexError: + utils.msg(irc, source, 'Error: not enough arguments. Needs 3: source nick, target, text.') + return + sourceuid = utils.nickToUid(irc, msgsource) + if not sourceuid: + utils.msg(irc, source, 'Error: unknown user %r' % msgsource) + return + if not utils.isChannel(target): + real_target = utils.nickToUid(irc, target) + if real_target is None: + utils.msg(irc, source, 'Error: unknown user %r' % target) + return + else: + real_target = target + if not text: + utils.msg(irc, source, 'Error: no text given.') + return + irc.proto.messageClient(irc, sourceuid, real_target, text) From d3ee7ed9186aaa7932acbe2769306dfa51950a00 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 15 Aug 2015 05:12:20 -0700 Subject: [PATCH 26/27] plugins/admin: fix command help for 'mode' --- plugins/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/admin.py b/plugins/admin.py index 1cbfdf3..8362a10 100644 --- a/plugins/admin.py +++ b/plugins/admin.py @@ -186,7 +186,7 @@ def showchan(irc, source, args): def mode(irc, source, args): """ - Admin-only. Sets modes on from , where is the nick of a PyLink client.""" + Admin-only. Sets modes on from , where is either the nick of a PyLink client, or the SID of a PyLink server.""" checkauthenticated(irc, source) try: modesource, target, modes = args[0], args[1], args[2:] From dd91b7e5a04321e0371d36618b50ceb3ca9b305f Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 15 Aug 2015 19:18:04 -0700 Subject: [PATCH 27/27] relay: more efficient getLocalUser Set the .remote attribute of each relay client to the original netname, user pair. --- plugins/relay.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 39356fc..1e2c047 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -145,7 +145,7 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident, host=host, realname=realname, modes=modes, ts=userobj.ts).uid - remoteirc.users[u].remote = irc.name + remoteirc.users[u].remote = (irc.name, user) away = userobj.away if away: remoteirc.proto.awayClient(remoteirc, u, away) @@ -163,21 +163,10 @@ def getLocalUser(irc, user, targetirc=None): representing the original user on the target network, similar to what getRemoteUser() does.""" # First, iterate over everyone! - remoteuser = None - for k, v in relayusers.items(): - if k[0] == irc.name: - # We don't need to do anything if the target users is on - # the same network as us. - continue - if v.get(irc.name) == user: - # If the stored pseudoclient UID for the kicked user on - # this network matches the target we have, set that user - # as the one we're kicking! It's a handful, but remember - # we're mapping (home network, UID) pairs to their - # respective relay pseudoclients on other networks. - remoteuser = k - log.debug('(%s) getLocalUser: found %s to correspond to %s.', irc.name, v, k) - break + try: + remoteuser = irc.users[user].remote + except (AttributeError, KeyError): + remoteuser = None log.debug('(%s) getLocalUser: remoteuser set to %r (looking up %s/%s).', irc.name, remoteuser, user, irc.name) if remoteuser: # If targetirc is given, we'll return simply the UID of the user on the @@ -430,9 +419,9 @@ def handle_kick(irc, source, command, args): remotechan, real_target, text) if target != irc.pseudoclient.uid and not irc.users[target].channels: - irc.proto.quitClient(irc, target, 'Left all shared channels.') remoteuser = getLocalUser(irc, target) del relayusers[remoteuser][irc.name] + irc.proto.quitClient(irc, target, 'Left all shared channels.') utils.add_hook(handle_kick, 'KICK')